วิธีเปรียบเทียบ oldValues ​​และ newValues ​​บน React Hooks useEffect?


178

สมมติว่าฉันมี 3 อินพุต: rate, sendAmount และ earnAmount ฉันใส่ 3 อินพุตนั้นไว้ใช้ กฎคือ:

  • ถ้า sendAmount เปลี่ยนแปลงฉันจะคำนวณ receiveAmount = sendAmount * rate
  • หากได้รับการเปลี่ยนแปลงฉันคำนวณ sendAmount = receiveAmount / rate
  • หากอัตราเปลี่ยนแปลงฉันคำนวณว่าreceiveAmount = sendAmount * rateเมื่อใดsendAmount > 0หรือคำนวณsendAmount = receiveAmount / rateเมื่อใดreceiveAmount > 0

นี่คือรหัสแซนด์บ็อกซ์https://codesandbox.io/s/pkl6vn7x6jเพื่อสาธิตปัญหา

มีวิธีที่จะเปรียบเทียบได้oldValuesและnewValuesต้องการบนcomponentDidUpdateแทนการขนย้ายวัสดุ 3 สำหรับกรณีนี้หรือไม่?

ขอบคุณ


นี่คือทางออกสุดท้ายของฉันกับusePrevious https://codesandbox.io/s/30n01w2r06

ในกรณีนี้ฉันไม่สามารถใช้หลายรายการได้useEffectเนื่องจากการเปลี่ยนแปลงแต่ละครั้งนำไปสู่การโทรในเครือข่ายเดียวกัน นั่นเป็นเหตุผลที่ฉันใช้changeCountเพื่อติดตามการเปลี่ยนแปลงด้วย changeCountนอกจากนี้ยังมีประโยชน์ในการติดตามการเปลี่ยนแปลงจากภายในเครื่องเท่านั้นดังนั้นฉันจึงสามารถป้องกันการโทรเครือข่ายที่ไม่จำเป็นเนื่องจากการเปลี่ยนแปลงจากเซิร์ฟเวอร์


componentDidUpdate ควรจะช่วยได้อย่างไร? คุณจะยังต้องเขียน 3 เงื่อนไขนี้
Estus Flask

ฉันได้เพิ่มคำตอบด้วยโซลูชันเสริม 2 แบบ หนึ่งในนั้นเหมาะกับคุณหรือไม่?
Ben Carp

คำตอบ:


249

คุณสามารถเขียน hook ที่กำหนดเองเพื่อจัดเตรียมอุปกรณ์ประกอบฉากก่อนหน้านี้โดยใช้useRef

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

แล้วใช้ใน useEffect

const Component = (props) => {
    const {receiveAmount, sendAmount } = props
    const prevAmount = usePrevious({receiveAmount, sendAmount});
    useEffect(() => {
        if(prevAmount.receiveAmount !== receiveAmount) {

         // process here
        }
        if(prevAmount.sendAmount !== sendAmount) {

         // process here
        }
    }, [receiveAmount, sendAmount])
}

อย่างไรก็ตามมันชัดเจนกว่าและน่าจะดีกว่าและชัดเจนกว่าในการอ่านและทำความเข้าใจหากคุณใช้สองรายการuseEffectแยกกันสำหรับแต่ละรหัสการเปลี่ยนแปลงที่คุณต้องการประมวลผลแยกกัน


8
ขอขอบคุณสำหรับหมายเหตุเกี่ยวกับการใช้สองuseEffectสายแยกกัน ไม่ทราบว่าคุณสามารถใช้งานได้หลายครั้ง!
JasonH

3
ฉันลองใช้รหัสด้านบน แต่ eslint เตือนฉันว่าuseEffectขาดการอ้างอิงprevAmount
Littlee

3
เนื่องจาก prevAmount เก็บค่าไว้สำหรับค่าของ state / props ก่อนหน้านี้คุณจึงไม่จำเป็นต้องส่งต่อเป็นค่าอ้างอิงเพื่อ useEffect และคุณสามารถปิดใช้งานคำเตือนนี้สำหรับบางกรณีได้ คุณสามารถอ่านโพสต์นี้เพื่อดูรายละเอียดเพิ่มเติม?
Shubham Khatri

1
รักสิ่งนี้ - useRef มาช่วยเสมอ
Fernando Rojo

@ShubhamKhatri: เหตุผลในการตัด useEffect ของ ref.current = value คืออะไร?
curly_brackets

54

ในกรณีที่ใครก็ตามกำลังมองหาเวอร์ชันการใช้งาน TypeScript

ใน.tsxโมดูล:

import { useEffect, useRef } from "react";

const usePrevious = <T extends unknown>(value: T): T | undefined => {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};

หรือใน.tsโมดูล:

import { useEffect, useRef } from "react";

const usePrevious = <T>(value: T): T | undefined => {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};

3
โปรดทราบว่าสิ่งนี้จะใช้ไม่ได้ในไฟล์ TSX เพื่อให้สิ่งนี้ทำงานในไฟล์ TSX ให้เปลี่ยนเพื่อconst usePrevious = <T extends any>(...ให้ล่ามเห็นว่า <T> ไม่ใช่ JSX และเป็นข้อ จำกัด ทั่วไป
apokryfos

1
คุณอธิบายได้ไหมว่าทำไมถึง<T extends {}>ไม่ทำงาน ดูเหมือนว่าจะใช้ได้ผลสำหรับฉัน แต่ฉันพยายามเข้าใจความซับซ้อนของการใช้งานแบบนั้น
Karthikeyan_kk

1
เป็นการดีกว่าที่จะขยายที่ไม่รู้จักหรือเพียงแค่ใส่เบ็ดลงใน.tsไฟล์ ถ้าคุณขยาย{}คุณจะได้รับข้อผิดพลาดถ้าคุณข้ามระบุ T usePrevious<T>ใน
fgblomqvist

1
np และใช่ดีเสมอที่จะทำให้มันสด / ถูกต้อง / เป็นปัจจุบันเนื่องจากหลายพันคนใช้คำตอบเหล่านี้เป็นข้อมูลอ้างอิง :)
fgblomqvist

1
@fgblomqvist อัปเดตคำตอบของฉัน ขอบคุณสำหรับคำติชมอีกครั้ง
SeedyROM

35

ตัวเลือกที่ 1 - เรียกใช้ useEffect เมื่อค่าเปลี่ยนแปลง

const Component = (props) => {

  useEffect(() => {
    console.log("val1 has changed");
  }, [val1]);

  return <div>...</div>;
};

การสาธิต

ตัวเลือกที่ 2 - useHasChanged hook

การเปรียบเทียบค่าปัจจุบันกับค่าก่อนหน้าเป็นรูปแบบทั่วไปและแสดงให้เห็นถึง hook ที่กำหนดเองของตัวเองซึ่งซ่อนรายละเอียดการนำไปใช้งาน

const Component = (props) => {
  const hasVal1Changed = useHasChanged(val1)

  useEffect(() => {
    if (hasVal1Changed ) {
      console.log("val1 has changed");
    }
  });

  return <div>...</div>;
};

const useHasChanged= (val: any) => {
    const prevVal = usePrevious(val)
    return prevVal !== val
}

const usePrevious = (value) => {
    const ref = useRef();
    useEffect(() => {
      ref.current = value;
    });
    return ref.current;
}


การสาธิต


ตัวเลือกที่สองใช้ได้ผลสำหรับฉัน คุณช่วยแนะนำฉันได้ไหมว่าทำไมฉันต้องเขียน useEffect Twice
Tarun Nagpal

@TarunNagpal คุณไม่จำเป็นต้องใช้ useEffect สองครั้ง ขึ้นอยู่กับกรณีการใช้งานของคุณ ลองนึกภาพว่าเราแค่ต้องการบันทึกว่า Val ตัวไหนเปลี่ยนไป หากเรามีทั้ง val1 และ val2 ในอาร์เรย์ของการอ้างอิงมันจะทำงานทุกครั้งที่มีการเปลี่ยนแปลงค่าใด ๆ จากนั้นภายในฟังก์ชั่นที่เราส่งเราจะต้องค้นหาว่า val ใดที่เปลี่ยนแปลงเพื่อบันทึกข้อความที่ถูกต้อง
Ben Carp

ขอบคุณสำหรับการตอบกลับ. เมื่อคุณพูดว่า "ในฟังก์ชั่นที่เราส่งผ่านเราจะต้องคิดว่าวาลใดมีการเปลี่ยนแปลง" เราจะบรรลุมันได้อย่างไร โปรดชี้แนะ
Tarun Nagpal

6

ฉันเพิ่งเผยแพร่react-deltaซึ่งแก้สถานการณ์แบบนี้ได้ ในความคิดของฉันuseEffectมีความรับผิดชอบมากเกินไป

หน้าที่ความรับผิดชอบ

  1. จะเปรียบเทียบค่าทั้งหมดในอาร์เรย์การอ้างอิงโดยใช้ Object.is
  2. มันเรียกใช้เอฟเฟกต์ / ล้างข้อมูลตามผลลัพธ์ของ # 1

การทำลายความรับผิดชอบ

react-deltaแบ่งuseEffectความรับผิดชอบออกเป็นตะขอเล็ก ๆ หลายอัน

ความรับผิดชอบ # 1

ความรับผิดชอบ # 2

จากประสบการณ์ของฉันแนวทางนี้มีความยืดหยุ่นสะอาดและรัดกุมกว่าuseEffect/ useRefวิธีแก้ปัญหา


สิ่งที่ฉันกำลังมองหา จะลองดู!
hackerl33t

ดูดีจริง ๆ แต่มันไม่ได้กินมากไปหน่อยเหรอ?
bluebird

@ ฮิซาโตะมันดีมากอาจจะมากเกินไป มันเป็น API รุ่นทดลอง และตรงไปตรงมาฉันไม่ได้ใช้มันมากนักในทีมของฉันเพราะมันไม่ได้เป็นที่รู้จักในวงกว้างหรือเป็นที่ยอมรับ ในทางทฤษฎีมันฟังดูดี แต่ในทางปฏิบัติอาจไม่คุ้มค่า
Austin Malerba

5

จากคำตอบที่ยอมรับซึ่งเป็นทางเลือกอื่นที่ไม่ต้องใช้ hook แบบกำหนดเอง:

const Component = (props) => {
  const { receiveAmount, sendAmount } = props;
  const prevAmount = useRef({ receiveAmount, sendAmount }).current;
  useEffect(() => {
    if (prevAmount.receiveAmount !== receiveAmount) {
     // process here
    }
    if (prevAmount.sendAmount !== sendAmount) {
     // process here
    }
    return () => { 
      prevAmount.receiveAmount = receiveAmount;
      prevAmount.sendAmount = sendAmount;
    };
  }, [receiveAmount, sendAmount]);
};

1
ทางออกที่ดี แต่มีข้อผิดพลาดทางไวยากรณ์ ไม่useRef userRefลืมใช้ปัจจุบันprevAmount.current
Blake Plumb

1
นี่เจ๋งสุด ๆ ถ้าฉันโหวตได้มากกว่านี้ฉันจะทำ! ก็สมเหตุสมผลเช่นกันคำตอบเดิมของฉันมาจากที่แห่งความไม่รู้ ฉันคิดว่านี่อาจเป็นรูปแบบที่เรียบง่ายและสง่างามที่สุดเท่าที่ฉันเคยเห็นมา
SeedyROM

1
คุณอาจสรุปสิ่งนี้ได้มากขึ้นเพื่อสร้างยูทิลิตี้ที่เรียบง่ายสุด ๆ
SeedyROM

3

เนื่องจากรัฐไม่ได้เป็นคู่แน่นด้วยเช่นองค์ประกอบในส่วนการทำงานของรัฐก่อนหน้านี้ไม่สามารถเข้าถึงได้ในโดยไม่บันทึกมันเป็นครั้งแรกเช่นกับuseEffect useRefนอกจากนี้ยังหมายความว่าการอัปเดตสถานะอาจถูกนำไปใช้อย่างไม่ถูกต้องในตำแหน่งที่ไม่ถูกต้องเนื่องจากสถานะก่อนหน้านี้มีอยู่ในsetStateฟังก์ชันตัวอัปเดต

นี่เป็นกรณีการใช้งานที่ดีสำหรับuseReducerการจัดเก็บที่เหมือน Redux และอนุญาตให้ใช้รูปแบบที่เกี่ยวข้อง การอัปเดตสถานะจะดำเนินการอย่างชัดเจนดังนั้นจึงไม่จำเป็นต้องคิดว่าจะอัปเดตทรัพย์สินของรัฐใด สิ่งนี้ชัดเจนแล้วจากการดำเนินการที่ส่งไป

นี่คือตัวอย่างลักษณะที่อาจมีลักษณะดังนี้:

function reducer({ sendAmount, receiveAmount, rate }, action) {
  switch (action.type) {
    case "sendAmount":
      sendAmount = action.payload;
      return {
        sendAmount,
        receiveAmount: sendAmount * rate,
        rate
      };
    case "receiveAmount":
      receiveAmount = action.payload;
      return {
        sendAmount: receiveAmount / rate,
        receiveAmount,
        rate
      };
    case "rate":
      rate = action.payload;
      return {
        sendAmount: receiveAmount ? receiveAmount / rate : sendAmount,
        receiveAmount: sendAmount ? sendAmount * rate : receiveAmount,
        rate
      };
    default:
      throw new Error();
  }
}

function handleChange(e) {
  const { name, value } = e.target;
  dispatch({
    type: name,
    payload: value
  });
}

...
const [state, dispatch] = useReducer(reducer, {
  rate: 2,
  sendAmount: 0,
  receiveAmount: 0
});
...

สวัสดี @estus ขอบคุณสำหรับความคิดนี้ มันทำให้ฉันมีวิธีคิดอีกแบบ แต่ฉันลืมบอกไปว่าฉันต้องเรียก API สำหรับแต่ละกรณี คุณมีทางออกสำหรับสิ่งนั้นหรือไม่?
rwinzhang

คุณหมายถึงอะไร? ที่จับด้านในเปลี่ยน? 'API' หมายถึงปลายทาง API ระยะไกลหรือไม่ คุณสามารถอัปเดต codeandbox จากคำตอบพร้อมรายละเอียดได้หรือไม่?
Estus Flask

จากการอัปเดตฉันสามารถสันนิษฐานได้ว่าคุณต้องการดึงข้อมูลsendAmountและอื่น ๆ แบบอะซิงโครนัสแทนที่จะคำนวณใช่ไหม สิ่งนี้เปลี่ยนแปลงไปมาก สามารถทำได้useReduceแต่อาจยุ่งยาก หากคุณจัดการกับ Redux ก่อนคุณอาจรู้ว่าการดำเนินการ async ไม่ได้ตรงไปตรงมาที่นั่น github.com/reduxjs/redux-thunkเป็นส่วนขยายยอดนิยมสำหรับ Redux ที่อนุญาตให้มีการทำงานแบบ async นี่คือการสาธิตที่ augments useReducer ที่มีรูปแบบเดียวกัน (useThunkReducer) codesandbox.io/s/6z4r79ymwr โปรดสังเกตว่าฟังก์ชันที่จัดส่งคือasyncคุณสามารถร้องขอได้ที่นั่น (แสดงความคิดเห็น)
Estus Flask

1
ปัญหาของคำถามคือคุณถามเกี่ยวกับสิ่งอื่นที่ไม่ใช่กรณีจริงของคุณและจากนั้นก็เปลี่ยนมันดังนั้นตอนนี้จึงเป็นคำถามที่แตกต่างกันมากในขณะที่คำตอบที่มีอยู่จะดูเหมือนว่าพวกเขาเพิกเฉยต่อคำถาม นี่ไม่ใช่แนวทางปฏิบัติที่แนะนำสำหรับ SO เนื่องจากไม่ได้ช่วยให้คุณได้รับคำตอบที่ต้องการ โปรดอย่าลบส่วนสำคัญออกจากคำถามที่มีคำตอบอยู่แล้วอ้างถึง (สูตรการคำนวณ) หากคุณยังคงพบปัญหา (หลังจากความคิดเห็นก่อนหน้าของฉัน (คุณน่าจะเป็น) ลองถามคำถามใหม่ที่ตรงกับกรณีของคุณและเชื่อมโยงไปยังคำถามนี้เป็นความพยายามครั้งก่อน
Estus Flask

สวัสดี estus ฉันคิดว่าโค้ดแซนด์บ็อกซ์นี้ codeandbox.io/s/6z4r79ymwrยังไม่ได้รับการอัปเดต ฉันจะเปลี่ยนกลับคำถามขอโทษที่
rwinzhang

3

การใช้ Ref จะนำเสนอบั๊กรูปแบบใหม่ในแอป

ลองดูกรณีนี้โดยใช้usePreviousที่มีคนแสดงความคิดเห็นก่อนหน้านี้:

  1. prop.minTime: 5 ==> ref.current = 5 | ตั้งค่าการอ้างอิงปัจจุบัน
  2. prop.minTime: 5 ==> ref.current = 5 | ค่าใหม่เท่ากับ ref.current
  3. prop.minTime: 8 ==> ref.current = 5 | ค่าใหม่ไม่เท่ากับ ref.current
  4. prop.minTime: 5 ==> ref.current = 5 | ค่าใหม่เท่ากับ ref.current

ดังที่เราเห็นที่นี่เราไม่ได้อัปเดตภายในrefเนื่องจากเราใช้useEffect


1
React มีตัวอย่างนี้ แต่พวกเขากำลังใช้state... ไม่ใช่props... เมื่อคุณสนใจเกี่ยวกับอุปกรณ์ประกอบฉากเก่าข้อผิดพลาดจะเกิดขึ้น
santomegonzalo

3

สำหรับการเปรียบเทียบเสาที่เรียบง่ายจริงๆคุณสามารถใช้ได้ useEffectเพื่อตรวจสอบได้อย่างง่ายดายเพื่อดูว่ามีการอัปเดตเสาหรือไม่

const myComponent = ({ prop }) => {
  useEffect(() => {
    ---Do stuffhere----
  }, [prop])
}

useEffect จากนั้นจะรันโค้ดของคุณก็ต่อเมื่อ prop เปลี่ยนไป


1
ใช้งานได้เพียงครั้งเดียวหลังจากpropการเปลี่ยนแปลงติดต่อกันคุณจะไม่สามารถทำได้~ do stuff here ~
Karolis Šarapnickis

2
ดูเหมือนว่าคุณควรเรียกsetHasPropChanged(false)เมื่อสิ้นสุด~ do stuff here ~การ "รีเซ็ต" สถานะของคุณ (แต่จะรีเซ็ตในการเรนเดอร์พิเศษ)
Kevin Wang

ขอบคุณสำหรับคำติชมคุณคิดถูกทั้งคู่มีการปรับปรุงโซลูชัน
Charlie Tupman

@ AntonioPavicevac-Ortiz ฉันได้อัปเดตคำตอบเพื่อแสดง propHasChanged เป็นจริงซึ่งจะเรียกมันว่าการแสดงผลครั้งเดียวอาจเป็นทางออกที่ดีกว่าเพียงแค่ตัด useEffect ออกและตรวจสอบ prop
Charlie Tupman

ฉันคิดว่าการใช้สิ่งนี้เดิมของฉันหายไปแล้ว เมื่อมองย้อนกลับไปที่รหัสคุณสามารถใช้ useEffect
Charlie Tupman

1

usePreviousนี่เป็นตะขอที่กำหนดเองที่ผมใช้ซึ่งผมเชื่อว่าจะง่ายกว่าการใช้

import { useRef, useEffect } from 'react'

// useTransition :: Array a => (a -> Void, a) -> Void
//                              |_______|  |
//                                  |      |
//                              callback  deps
//
// The useTransition hook is similar to the useEffect hook. It requires
// a callback function and an array of dependencies. Unlike the useEffect
// hook, the callback function is only called when the dependencies change.
// Hence, it's not called when the component mounts because there is no change
// in the dependencies. The callback function is supplied the previous array of
// dependencies which it can use to perform transition-based effects.
const useTransition = (callback, deps) => {
  const func = useRef(null)

  useEffect(() => {
    func.current = callback
  }, [callback])

  const args = useRef(null)

  useEffect(() => {
    if (args.current !== null) func.current(...args.current)
    args.current = deps
  }, deps)
}

คุณจะใช้useTransitionดังต่อไปนี้

useTransition((prevRate, prevSendAmount, prevReceiveAmount) => {
  if (sendAmount !== prevSendAmount || rate !== prevRate && sendAmount > 0) {
    const newReceiveAmount = sendAmount * rate
    // do something
  } else {
    const newSendAmount = receiveAmount / rate
    // do something
  }
}, [rate, sendAmount, receiveAmount])

หวังว่าจะช่วยได้


1

หากคุณต้องการuseEffectแนวทางการเปลี่ยน:

const usePreviousEffect = (fn, inputs = []) => {
  const previousInputsRef = useRef([...inputs])

  useEffect(() => {
    fn(previousInputsRef.current)
    previousInputsRef.current = [...inputs]
  }, inputs)
}

และใช้มันดังนี้:

usePreviousEffect(
  ([prevReceiveAmount, prevSendAmount]) => {
    if (prevReceiveAmount !== receiveAmount) // side effect here
    if (prevSendAmount !== sendAmount) // side effect here
  },
  [receiveAmount, sendAmount]
)

โปรดทราบว่าในครั้งแรกที่เอฟเฟกต์ทำงานค่าก่อนหน้านี้ที่ส่งผ่านไปยังของคุณfnจะเหมือนกับค่าอินพุตเริ่มต้นของคุณ สิ่งนี้จะสำคัญสำหรับคุณก็ต่อเมื่อคุณต้องการทำบางสิ่งบางอย่างเมื่อค่าไม่เปลี่ยนแปลง


โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.