Bagaimana cara membandingkan oldValues ​​dan newValues ​​di React Hooks useEffect?

178

Katakanlah saya memiliki 3 input: rate, sendAmount, dan acceptAmount. Saya menempatkan 3 masukan itu pada parameter diffing useEffect. Aturannya adalah:

  • Jika sendAmount berubah, saya menghitung receiveAmount = sendAmount * rate
  • Jika acceptAmount berubah, saya menghitung sendAmount = receiveAmount / rate
  • Jika tarif berubah, saya menghitung receiveAmount = sendAmount * ratekapan sendAmount > 0atau saya menghitung sendAmount = receiveAmount / ratekapanreceiveAmount > 0

Berikut adalah codesandbox https://codesandbox.io/s/pkl6vn7x6j untuk mendemonstrasikan masalahnya.

Adakah cara untuk membandingkan oldValuesdan newValuessuka componentDidUpdatedaripada membuat 3 penangan untuk kasus ini?

Terima kasih


Inilah solusi terakhir saya dengan usePrevious https://codesandbox.io/s/30n01w2r06

Dalam kasus ini, saya tidak dapat menggunakan beberapa useEffectkarena setiap perubahan mengarah ke panggilan jaringan yang sama. Itu sebabnya saya juga biasa changeCountmelacak perubahannya. Ini changeCountjuga membantu untuk melacak perubahan dari hanya lokal, jadi saya dapat mencegah panggilan jaringan yang tidak perlu karena perubahan dari server.

rwinzhang
sumber
Bagaimana tepatnya componentDidUpdate seharusnya membantu? Anda masih perlu menulis 3 ketentuan ini.
Estus Flask
Saya menambahkan jawaban dengan 2 solusi opsional. Apakah salah satunya cocok untuk Anda?
Ben Carp

Jawaban:

249

Anda dapat menulis hook kustom untuk memberi Anda props sebelumnya menggunakan useRef

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

lalu gunakan di 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])
}

Namun lebih jelas dan mungkin lebih baik dan lebih jelas untuk membaca dan memahami jika Anda menggunakan dua useEffectsecara terpisah untuk setiap id perubahan, Anda ingin memprosesnya secara terpisah

Shubham Khatri
sumber
8
Terima kasih atas catatan tentang menggunakan dua useEffectpanggilan secara terpisah. Tidak sadar Anda bisa menggunakannya berkali-kali!
JasonH
3
Saya mencoba kode di atas, tetapi eslint memperingatkan saya bahwa useEffectdependensi hilangprevAmount
Littlee
3
Karena prevAmount menyimpan nilai untuk nilai state / props sebelumnya, Anda tidak perlu meneruskannya sebagai dependecy untuk useEffect dan Anda dapat menonaktifkan peringatan ini untuk kasus tertentu. Anda dapat membaca posting ini untuk lebih jelasnya?
Shubham Khatri
1
Suka ini - useRef selalu datang untuk menyelamatkan.
Fernando Rojo
@ShubhamKhatri: Apa alasan pembungkusan useEffect ref.current = value?
curly_brackets
54

Jika ada yang mencari versi TypeScript dari usePrevious:

Dalam .tsxmodul:

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;
};

Atau dalam .tsmodul:

import { useEffect, useRef } from "react";

const usePrevious = <T>(value: T): T | undefined => {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};
SeedyROM
sumber
3
Perhatikan bahwa ini tidak akan berfungsi dalam file TSX, untuk const usePrevious = <T extends any>(...membuatnya berfungsi dalam file TSX, ubah untuk membuat penerjemah melihat bahwa <T> bukan JSX dan merupakan batasan umum
apokryfos
1
Bisakah Anda menjelaskan mengapa <T extends {}>tidak akan berhasil. Tampaknya berfungsi untuk saya tetapi saya mencoba memahami kompleksitas menggunakannya seperti itu.
Karthikeyan_kk
1
Lebih baik untuk memperluas tidak diketahui, atau hanya memasukkan hook ke dalam .tsfile. Jika Anda memperpanjang {}Anda akan mendapatkan kesalahan jika Anda melewatkan menentukan T in usePrevious<T>.
fgblomqvist
1
np, dan ya, selalu menyenangkan untuk menjaganya tetap segar / benar / terbaru karena ribuan orang menggunakan jawaban ini sebagai referensi :)
fgblomqvist
1
@fgblomqvist Memperbarui jawaban saya. Terima kasih atas umpan baliknya lagi.
SeedyROM
35

Opsi 1 - jalankan useEffect saat nilai berubah

const Component = (props) => {

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

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

Demo

Opsi 2 - hook useHasChanged

Membandingkan nilai saat ini dengan nilai sebelumnya adalah pola umum, dan membenarkan kaitan khusus miliknya yang menyembunyikan detail implementasi.

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;
}


Demo

Ben Carp
sumber
Pilihan kedua berhasil untuk saya. Bisakah Anda membimbing saya mengapa saya harus menulis useEffect Twice?
Tarun Nagpal
@TarunNagpal, Anda tidak perlu menggunakan useEffect dua kali. Itu tergantung pada kasus penggunaan Anda. Bayangkan kita hanya ingin mencatat nilai yang telah berubah. Jika kita memiliki val1 dan val2 dalam larik dependensi kita, ini akan berjalan setiap kali ada nilai yang berubah. Kemudian di dalam fungsi yang kita lewati, kita harus mencari tahu val mana yang telah berubah untuk mencatat pesan yang benar.
Ben Carp
Terima kasih balasannya. Ketika Anda mengatakan "di dalam fungsi yang kita lewati, kita harus mencari tahu val mana yang berubah". Bagaimana kita bisa mencapainya. Harap memandu
Tarun Nagpal
6

Saya baru saja menerbitkan react-delta yang memecahkan skenario semacam ini. Menurut saya, useEffecttanggung jawabnya terlalu banyak.

Tanggung jawab

  1. Ini membandingkan semua nilai dalam larik ketergantungannya menggunakan Object.is
  2. Ini menjalankan callback efek / pembersihan berdasarkan hasil # 1

Memutus Tanggung Jawab

react-deltamemecah useEffecttanggung jawab menjadi beberapa kait yang lebih kecil.

Tanggung jawab # 1

Tanggung jawab # 2

Menurut pengalaman saya, pendekatan ini lebih fleksibel, bersih, dan ringkas daripada useEffect/ useRefsolutions.

Austin Malerba
sumber
Hanya apa yang saya cari. Akan mencobanya!
hackerl33t
terlihat sangat bagus tapi bukankah itu terlalu berlebihan?
bluebird
@Hisato itu sangat mungkin berlebihan. Ini adalah semacam API eksperimental. Dan sejujurnya saya belum banyak menggunakannya dalam tim saya karena tidak banyak dikenal atau diadopsi. Secara teori kedengarannya bagus, tetapi dalam praktiknya mungkin tidak sepadan.
Austin Malerba
5

Keluar dari jawaban yang diterima, solusi alternatif yang tidak memerlukan pengait khusus:

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]);
};
Drazen Bjelovuk
sumber
1
Solusi bagus, tetapi mengandung kesalahan sintaks. useReftidak userRef. Lupa menggunakan arusprevAmount.current
Blake Plumb
1
Ini sangat keren, jika saya dapat memberi suara lebih banyak, saya akan melakukannya! Masuk akal juga, jawaban asli saya berasal dari tempat ketidaktahuan. Saya pikir ini mungkin pola paling sederhana dan paling elegan yang pernah saya lihat sejauh ini.
SeedyROM
1
Anda mungkin dapat mengabstraksi ini lebih jauh untuk membuat utilitas super sederhana.
SeedyROM
3

Karena status tidak digabungkan erat dengan instance komponen dalam komponen fungsional, status sebelumnya tidak dapat dicapai useEffecttanpa menyimpannya terlebih dahulu, misalnya dengan useRef. Ini juga berarti bahwa pembaruan status mungkin salah diimplementasikan di tempat yang salah karena status sebelumnya tersedia di dalam setStatefungsi updater.

Ini adalah kasus penggunaan yang baik useReduceryang menyediakan penyimpanan mirip Redux dan memungkinkan untuk mengimplementasikan pola masing-masing. Pembaruan status dilakukan secara eksplisit, jadi tidak perlu mencari tahu properti negara mana yang diperbarui; ini sudah jelas dari tindakan yang dikirim.

Berikut adalah contoh tampilannya:

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 Flask
sumber
Hai @estus, terima kasih atas ide ini. Ini memberi saya cara berpikir lain. Tapi, saya lupa menyebutkan bahwa saya perlu memanggil API untuk setiap kasus. Apakah Anda punya solusi untuk itu?
rwinzhang
Apa maksudmu? Inside handleChange? Apakah 'API' berarti titik akhir API jarak jauh? Bisakah Anda memperbarui kode dan kotak dari jawaban dengan detail?
Estus Flask
Dari pembaruan saya dapat berasumsi bahwa Anda ingin mengambil sendAmount, dll secara asinkron alih-alih menghitungnya, bukan? Ini banyak berubah. Ini mungkin untuk dilakukan dengan useReducetetapi mungkin rumit. Jika Anda pernah berurusan dengan Redux sebelumnya, Anda mungkin tahu bahwa operasi asinkron tidak langsung ke sana. github.com/reduxjs/redux-thunk adalah ekstensi populer untuk Redux yang memungkinkan tindakan asinkron. Berikut adalah demo yang menambahkan useReducer dengan pola yang sama (useThunkReducer), codesandbox.io/s/6z4r79ymwr . Perhatikan bahwa fungsi dispatched adalah async, Anda dapat melakukan permintaan di sana (berkomentar).
Estus Flask
1
Masalah dengan pertanyaan ini adalah Anda bertanya tentang hal lain yang bukan kasus Anda yang sebenarnya dan kemudian mengubahnya, jadi sekarang pertanyaan itu sangat berbeda sementara jawaban yang ada muncul seolah-olah mereka mengabaikan pertanyaan itu. Ini bukan praktik yang direkomendasikan pada SO karena tidak membantu Anda mendapatkan jawaban yang Anda inginkan. Tolong, jangan hapus bagian penting dari pertanyaan yang sudah dirujuk oleh jawaban (rumus perhitungan). Jika Anda masih memiliki masalah (setelah komentar saya sebelumnya (kemungkinan besar Anda melakukannya), pertimbangkan untuk mengajukan pertanyaan baru yang mencerminkan kasus Anda dan tautkan ke pertanyaan ini sebagai upaya Anda sebelumnya.
Estus Flask
Halo estus, saya pikir ini codesandbox codesandbox.io/s/6z4r79ymwr belum diperbarui. Saya akan mengubah kembali pertanyaannya, maaf untuk itu.
rwinzhang
3

Menggunakan Ref akan memperkenalkan jenis bug baru ke dalam aplikasi.

Mari kita lihat kasus ini menggunakan usePreviouskomentar seseorang sebelumnya:

  1. prop.minTime: 5 ==> ref.current = 5 | atur ref. arus
  2. prop.minTime: 5 ==> ref.current = 5 | nilai baru sama dengan ref.current
  3. prop.minTime: 8 ==> ref.current = 5 | nilai baru TIDAK sama dengan ref.current
  4. prop.minTime: 5 ==> ref.current = 5 | nilai baru sama dengan ref.current

Seperti yang bisa kita lihat di sini, kita tidak memperbarui internal refkarena kita sedang menggunakanuseEffect

santomegonzalo
sumber
1
React memiliki contoh ini tetapi mereka menggunakan state... dan bukan props... ketika Anda peduli dengan props lama maka kesalahan akan terjadi.
santomegonzalo
3

Untuk perbandingan prop yang sangat sederhana, Anda dapat menggunakan useEffectuntuk memeriksa dengan mudah apakah sebuah prop telah diperbarui.

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

useEffect hanya akan menjalankan kode Anda jika prop berubah.

Charlie Tupman
sumber
1
Ini hanya berfungsi sekali, setelah propperubahan berturut-turut Anda tidak akan dapat~ do stuff here ~
Karolis Šarapnickis
2
Sepertinya Anda harus menelepon setHasPropChanged(false)di akhir ~ do stuff here ~untuk "mengatur ulang" negara Anda. (Tapi ini akan mengatur ulang di rerender ekstra)
Kevin Wang
Terima kasih atas umpan baliknya, Anda berdua benar, solusi yang diperbarui
Charlie Tupman
@ AntonioPavicevac-Ortiz Saya telah memperbarui jawaban untuk sekarang membuat propHasChanged sebagai benar yang kemudian akan menyebutnya sekali pada render, mungkin menjadi solusi yang lebih baik hanya untuk merobek useEffect dan hanya memeriksa prop
Charlie Tupman
Saya pikir penggunaan asli saya ini telah hilang. Melihat kembali kode, Anda bisa menggunakan useEffect
Charlie Tupman
1

Berikut hook khusus yang saya gunakan yang menurut saya lebih intuitif daripada menggunakan 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)
}

Anda akan menggunakan useTransitionsebagai berikut.

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])

Semoga membantu.

Aadit M Shah
sumber
1

Jika Anda lebih suka useEffectpendekatan pengganti:

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

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

Dan gunakan seperti ini:

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

Perhatikan bahwa pertama kali efek dijalankan, nilai sebelumnya yang diteruskan ke Anda fnakan sama dengan nilai masukan awal Anda. Ini hanya penting bagi Anda jika Anda ingin melakukan sesuatu ketika nilainya tidak berubah.

J. VanLeeuwen
sumber