React: useCallback / useEffect mendukung komparator khusus

Dibuat pada 20 Des 2018  ·  30Komentar  ·  Sumber: facebook/react

Saat ini kita dapat mengirimkan array sebagai argumen kedua saat menggunakan useCallback atau useEffect seperti di bawah ini:

useCallback(()=> {
  doSth(a, b)
}, [a, b]) // how to do deep equal if a is an object ?

Masalahnya adalah itu hanya membandingkan item array dengan === , adakah cara untuk membandingkan objek yang kompleks?

Mendukung pembanding khusus karena argumen ketiga terlihat tidak buruk:

useCallback(()=> {
  doSth(a, b)
  }, 
  [complexObject], 
  (item, previousItem)=> { //custom compare logic, return true || false here }
)
Hooks Question

Komentar yang paling membantu

Saat ini kami tidak memiliki rencana untuk melakukan ini. Perbandingan khusus bisa menjadi sangat mahal jika disebarkan ke seluruh komponen - terutama saat orang mencoba melakukan pemeriksaan kesetaraan yang mendalam di sana. Biasanya ada cara berbeda untuk menyusun kode sehingga Anda tidak membutuhkannya.

Ada beberapa solusi umum:

Opsi 1: Angkat

Jika beberapa nilai seharusnya selalu statis, nilai itu tidak perlu ada di fungsi render sama sekali.
Sebagai contoh:

const useFetch = createFetch({ /* static config */});

function MyComponent() {
  const foo = useFetch();
}

Itu benar-benar menyelesaikan masalah. Jika membuat instance menjengkelkan, Anda selalu dapat memiliki modul useFetch Anda sendiri yang hanya mengekspor ulang hasil yang dibuat (atau bahkan menyediakan beberapa variasi):

// ./myUseFetch.js
import createFetch from 'some-library';

export const useFetch = createFetch({ /* config 1 */ })
export const useFetchWithAuth = createFetch({ /* config 2 */ })

Opsi 2: Rangkullah nilai-nilai dinamis

Pilihan lainnya adalah sebaliknya. Sadarilah bahwa nilai-nilai selalu dinamis, dan minta konsumen API untuk memintanya.

function MyComponent() {
  const config = useMemo(() => ({
    foo: 42,
  }, []); // Memoized
  const data = useFetch(url, config);
}

Ini mungkin tampak berbelit-belit. Namun untuk Hook level rendah, Anda mungkin akan membuat pembungkus level yang lebih tinggi. Jadi mereka bisa bertanggung jawab untuk memoisasi yang benar.

// ./myUseFetch.js
import {useContext} from 'react';
import useFetch from 'some-library';
import AuthContext from './auth-context';

export function useFetchWithAuth(url) {
  const authToken = useContext(AuthContext);
  const config = useMemo(() => ({
    foo: 42,
    auth: authToken
  }), [authToken]); // Only changes the config if authToken changes
  return useFetch(url, config);
}

// ./MyComponent.js
function MyComponent() {
  const data = useFethWithAuth('someurl.com'); // No config
}

Kemudian secara efektif pengguna tidak perlu mengonfigurasi apa pun di situs panggilan - atau Anda dapat menampilkan opsi yang didukung terbatas sebagai argumen khusus. Itu mungkin API yang lebih baik daripada objek "opsi" yang sewenang-wenang.

Opsi 3: (hati-hati) JSON.stringify

Banyak orang mengungkapkan keinginannya untuk pemeriksaan kesetaraan yang mendalam. Namun, pemeriksaan kesetaraan yang mendalam sangat buruk karena kinerjanya tidak dapat diprediksi. Mereka bisa menjadi cepat suatu hari, dan kemudian Anda mengubah sedikit struktur datanya, dan mereka menjadi sangat lambat karena kompleksitas traversal telah berubah. Jadi kami ingin secara eksplisit mencegah penggunaan pemeriksaan mendalam - dan useEffect API saat ini melakukannya.

Namun, ada beberapa kasus di mana hal itu terlalu menghalangi ergonomi. Semisal dengan kode like

const variables = {userId: id};
const data = useQuery(variables);

Dalam kasus ini, sebenarnya tidak diperlukan perbandingan mendalam. Dalam contoh ini, kami mengetahui bahwa struktur data kami relatif dangkal, tidak memiliki siklus, dan dapat diserialkan dengan mudah (misalnya karena kami berencana untuk mengirim struktur data tersebut melalui jaringan sebagai permintaan).

Dalam kasus yang jarang terjadi, dapat diterima untuk memberikan [JSON.stringify(variables)] sebagai dependensi.

"Tunggu!" Saya mendengar Anda berkata, "Bukankah JSON.stringify() lambat?"

Pada input kecil, seperti contoh di atas, ini sangat cepat.

Itu menjadi lambat pada input yang lebih besar. Tetapi pada saat itu, kesetaraan yang dalam juga cukup lambat sehingga Anda ingin memikirkan kembali strategi dan API Anda. Seperti kembali ke pembatalan granular dengan useMemo . Itu memberi Anda kinerja - jika memang, Anda peduli tentangnya.


Mungkin ada kasus penggunaan kecil lainnya yang tidak sesuai dengan ini. Untuk kasus penggunaan tersebut, dapat diterima untuk menggunakan ref sebagai jalan keluar. Tetapi Anda harus sangat mempertimbangkan untuk menghindari membandingkan nilai secara manual, dan memikirkan kembali API Anda jika Anda merasa membutuhkannya.

Semua 30 komentar

Anda bisa melakukan ini sebagai gantinya.

useCallback(() => {
  doSth(a, b)
}, [a, a.prop, a.anotherProp, a.prop.nestedProp /* and so on */, b])

Atau Anda dapat mencoba menggunakan JSON.stringify(a) dalam larik input.

Anda dapat menggunakan hasil pemeriksaan kesetaraan kustom sebagai nilai di dalam larik untuk mendapatkan perilaku ini.

useEffect(
  () => {
    // ...
  },
  [compare(a, b)]
);

@aweary Bagaimana cara kerjanya? Mereka ingin menggunakan pembanding khusus untuk memeriksa compare(a, prevA) , bukan compare(a, b) . Nilai sebelumnya tidak dapat diakses dalam cakupan.

@heyimalex Anda dapat menggunakan ref untuk menyimpan referensi ke nilai sebelumnya. Ini disebutkan di FAQ Hooks: Bagaimana cara mendapatkan properti atau status sebelumnya?

@aweary Bagaimana lulus [compare(a, prevA)] bekerja? Bukankah itu akan membuat pembaruan pada transisi dari true -> false dan dari false -> true ketika Anda benar-benar hanya ingin memperbarui ketika false ? Maaf jika saya melewatkan sesuatu yang jelas!

Saya mencoba untuk menulis apa yang diinginkan op dan inilah yang saya dapatkan.

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

function useCallbackCustomEquality(cb, args, equalityFunc) {
    const prevArgs = usePrevious(args);
    const argsAreEqual =
        prevArgs !== undefined &&
        args.length === prevArgs.length &&
        args.every((v, i) => equalityFunc(v, prevArgs[i]));
    const callbackRef = useRef();
    useEffect(() => {
        if (!argsAreEqual) {
            callbackRef.current = cb;
        }
    })
    return argsAreEqual ? callbackRef.current : cb;
}

@kpaxqin ya itu semacam solusi setengah matang 😕 pendekatan Anda baik-baik saja Saya pikir untuk useCallback . Untuk useEffect Anda akan melakukan sesuatu yang serupa kecuali Anda akan menambahkan centang untuk kesetaraan di dalam useEffect sebelum memanggil efek panggilan balik yang disediakan.

Anda sebenarnya bisa merangkum memoisasi arg dalam satu hook dan kemudian menggunakannya untuk semua fungsi lainnya.

function useMemoizeArgs(args, equalityCheck) {
  const ref = useRef();
  const prevArgs = ref.current;
  const argsAreEqual =
        prevArgs !== undefined &&
        args.length === prevArgs.length &&
        args.every((v, i) => equalityFunc(v, prevArgs[i]));
  useEffect(() => {
      if (!argsAreEqual) {
        ref.current = args;
      }
  });
  return argsAreEqual ? prevArgs : args;
}

useEffect(() => {
  // ...
}, useMemoizeArgs([a,b], equalityCheck));

API yang mirip dengan React.memo akan bagus untuk useEffect dan teman-teman saat digunakan dengan pustaka struktur data yang tidak dapat diubah - seperti clojurescript. Bukan untuk menyederhanakannya secara berlebihan, tetapi tampaknya ini merupakan perubahan yang sangat mudah diakses: https://github.com/facebook/react/blob/659c13963ea2b8a20e0b19d817ca40a3ebb19024/packages/react-reconciler/src/ReactFiberHooks.js#L524 -L549

@ heyimalex Terima kasih atas solusinya, ini berfungsi dengan baik untuk saya, tetapi masih ada satu risiko sebagai solusi umum.

Misalnya: render ulang setiap detik, gunakan waktu saat ini sebagai larik input, bandingkan waktu dan jalankan efek jika menit telah berubah.

Kuncinya adalah: A equal to B dan B equal to C mungkin tidak berarti A equal to C untuk pembanding khusus

Gunakan nilai lain karena hasil perbandingan bisa menghilangkan ini

function useMemoizeArgs(args, equalityCheck) {
  const ref = useRef();
  const flag = ref.current ? ref.current.flag : 0;
  const prevArgs = ref.current ? ref.current.args : undefined;
  const argsAreEqual =
        prevArgs !== undefined &&
        args.length === prevArgs.length &&
        args.every((v, i) => equalityFunc(v, prevArgs[i]));
  useEffect(() => {
      ref.current = {
         args,
         flag: !argsAreEqual ? (flag + 1) : flag
      }
  });
  return ref.current.flag
}

useEffect(() => {
  // ...
}, useMemoizeArgs([a,b], equalityCheck));

Masih berpikir cara terbaik adalah mendukung komparator khusus secara resmi untuk pengait yang perlu membandingkan input.

Solusi yang diberikan oleh @kpaxqin tidak berfungsi selama pengujian saya karena:

  1. ref.current tidak ditentukan pada panggilan pertama jadi return ref.current.flag crash
  2. cara argsAreEqual dihitung akhirnya mengembalikan true untuk panggilan pertama, oleh karena itu flag selalu bertambah setidaknya sekali

Saya memodifikasinya menjadi:
`` js
const useMemoizeArgs = (args, equalityFunc) => {
const ref = useRef ();
const flag = ref.current? ref.current.flag: 0;
const prevArgs = ref.current? ref.current.args: tidak ditentukan;
const argsHaveChanged =
prevArgs! == undefined &&
(args.length! == prevArgs.length || args.some ((v, i) => equalityFunc (v, prevArgs [i])));

useEffect (() => {
ref.current = {
args,
flag: argsHaveChanged? bendera + 1: bendera,
};
});

bendera kembali;
};
`` ''

yang berfungsi untuk kasus penggunaan saya.

Tetapi jelas jika hook mendukung pembanding khusus seperti React.memo sebagai parameter, semuanya akan jauh lebih baik / sederhana.

Kasus penggunaan utama saya di mana persamaan yang diperlukan adalah pengambilan data

const useFetch = (config) => {
  const [data, setData] = useState(null); 
  useEffect(() => {
    axios(config).then(response => setState(response.data)
  }, [config]);  // <-- will fetch on each render
  return [data];
}

// in render
const DocumentList = ({ archived }) => {
  const data = useFetch({url: '/documents/', method: "GET", params: { archived }, timeout: 1000 }) 
  ...
}

Saya memeriksa hasil google teratas untuk "react use fetch hook", mereka memiliki bug (ambil ulang di setiap rerender) atau menggunakan salah satu metode:

Jadi, sebaiknya memiliki pembanding sebagai argumen ketiga atau bagian dalam dokumen yang menjelaskan cara yang benar untuk kasus penggunaan tersebut.

Saat ini kami tidak memiliki rencana untuk melakukan ini. Perbandingan khusus bisa menjadi sangat mahal jika disebarkan ke seluruh komponen - terutama saat orang mencoba melakukan pemeriksaan kesetaraan yang mendalam di sana. Biasanya ada cara berbeda untuk menyusun kode sehingga Anda tidak membutuhkannya.

Ada beberapa solusi umum:

Opsi 1: Angkat

Jika beberapa nilai seharusnya selalu statis, nilai itu tidak perlu ada di fungsi render sama sekali.
Sebagai contoh:

const useFetch = createFetch({ /* static config */});

function MyComponent() {
  const foo = useFetch();
}

Itu benar-benar menyelesaikan masalah. Jika membuat instance menjengkelkan, Anda selalu dapat memiliki modul useFetch Anda sendiri yang hanya mengekspor ulang hasil yang dibuat (atau bahkan menyediakan beberapa variasi):

// ./myUseFetch.js
import createFetch from 'some-library';

export const useFetch = createFetch({ /* config 1 */ })
export const useFetchWithAuth = createFetch({ /* config 2 */ })

Opsi 2: Rangkullah nilai-nilai dinamis

Pilihan lainnya adalah sebaliknya. Sadarilah bahwa nilai-nilai selalu dinamis, dan minta konsumen API untuk memintanya.

function MyComponent() {
  const config = useMemo(() => ({
    foo: 42,
  }, []); // Memoized
  const data = useFetch(url, config);
}

Ini mungkin tampak berbelit-belit. Namun untuk Hook level rendah, Anda mungkin akan membuat pembungkus level yang lebih tinggi. Jadi mereka bisa bertanggung jawab untuk memoisasi yang benar.

// ./myUseFetch.js
import {useContext} from 'react';
import useFetch from 'some-library';
import AuthContext from './auth-context';

export function useFetchWithAuth(url) {
  const authToken = useContext(AuthContext);
  const config = useMemo(() => ({
    foo: 42,
    auth: authToken
  }), [authToken]); // Only changes the config if authToken changes
  return useFetch(url, config);
}

// ./MyComponent.js
function MyComponent() {
  const data = useFethWithAuth('someurl.com'); // No config
}

Kemudian secara efektif pengguna tidak perlu mengonfigurasi apa pun di situs panggilan - atau Anda dapat menampilkan opsi yang didukung terbatas sebagai argumen khusus. Itu mungkin API yang lebih baik daripada objek "opsi" yang sewenang-wenang.

Opsi 3: (hati-hati) JSON.stringify

Banyak orang mengungkapkan keinginannya untuk pemeriksaan kesetaraan yang mendalam. Namun, pemeriksaan kesetaraan yang mendalam sangat buruk karena kinerjanya tidak dapat diprediksi. Mereka bisa menjadi cepat suatu hari, dan kemudian Anda mengubah sedikit struktur datanya, dan mereka menjadi sangat lambat karena kompleksitas traversal telah berubah. Jadi kami ingin secara eksplisit mencegah penggunaan pemeriksaan mendalam - dan useEffect API saat ini melakukannya.

Namun, ada beberapa kasus di mana hal itu terlalu menghalangi ergonomi. Semisal dengan kode like

const variables = {userId: id};
const data = useQuery(variables);

Dalam kasus ini, sebenarnya tidak diperlukan perbandingan mendalam. Dalam contoh ini, kami mengetahui bahwa struktur data kami relatif dangkal, tidak memiliki siklus, dan dapat diserialkan dengan mudah (misalnya karena kami berencana untuk mengirim struktur data tersebut melalui jaringan sebagai permintaan).

Dalam kasus yang jarang terjadi, dapat diterima untuk memberikan [JSON.stringify(variables)] sebagai dependensi.

"Tunggu!" Saya mendengar Anda berkata, "Bukankah JSON.stringify() lambat?"

Pada input kecil, seperti contoh di atas, ini sangat cepat.

Itu menjadi lambat pada input yang lebih besar. Tetapi pada saat itu, kesetaraan yang dalam juga cukup lambat sehingga Anda ingin memikirkan kembali strategi dan API Anda. Seperti kembali ke pembatalan granular dengan useMemo . Itu memberi Anda kinerja - jika memang, Anda peduli tentangnya.


Mungkin ada kasus penggunaan kecil lainnya yang tidak sesuai dengan ini. Untuk kasus penggunaan tersebut, dapat diterima untuk menggunakan ref sebagai jalan keluar. Tetapi Anda harus sangat mempertimbangkan untuk menghindari membandingkan nilai secara manual, dan memikirkan kembali API Anda jika Anda merasa membutuhkannya.

Sekadar berkomentar bahwa ini adalah masalah dengan ClojureScript yang menyediakan semantik kesetaraannya sendiri, di mana perbandingan kesetaraan struktur data didasarkan pada nilai dan bukan referensi. Jadi menggunakan algoritma Object.is tanpa jalan keluar merupakan sebuah masalah. Demikian pula untuk setState.

@gaearon Option 3 sederhana dan berfungsi dengan baik untuk kasus saya tetapi bentrok dengan react-hooks/exhaustive-deps karena objek non-serial digunakan dalam efeknya. Apakah tidak mungkin dengan opsi linting ini diaktifkan?

@gaearon Option 3 sederhana dan berfungsi dengan baik untuk kasus saya tetapi bentrok dengan react-hooks/exhaustive-deps karena objek non-serial digunakan dalam efeknya. Apakah tidak mungkin dengan opsi linting ini diaktifkan?

Salah satu cara untuk menyiasatinya adalah dengan benar-benar men-deserialisasi objek dari string di dalam closure alih-alih menggunakan objek yang sudah di-deserialisasi ... tampaknya konyol dan membuang-buang siklus tetapi dalam arti tertentu pendekatan konseptual yang lebih murni sesuai dengan hook aturan.

Saya baru saja menerbitkan react-delta yang bertujuan untuk memecah useEffect menjadi beberapa kait yang lebih kecil untuk memberi pengembang lebih banyak kendali atas saat efek dijalankan. Ini agak eksperimental, dan saya pikir ini adalah salah satu libs di mana akan sulit untuk mengatakan nilainya sampai digunakan di dunia nyata. useEffect sebagian besar bersifat membatasi sedangkan react-delta memberi pengembang lebih banyak kekuatan. Kekuatan ini bisa disalahgunakan dengan sangat baik, tetapi jika orang akan berusaha sekuat tenaga untuk mendapatkan pemeriksaan perbandingan yang mendalam dari useEffect , maka saya pikir memiliki API khusus untuk memfasilitasi kontrol yang lebih halus atas perbandingan membuat merasakan. Menurut pendapat saya, useEffect agak terlalu membatasi dan memiliki terlalu banyak tanggung jawab.

Tanggung jawab

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

Tanggung Jawab Partisi

react-delta membagi tanggung jawab useEffect menjadi beberapa bagian yang lebih kecil.

Tanggung jawab 1

Tanggung jawab 2

Contoh

import React, { useState, useEffect } from "react";
import { useDeltaArray, some, useConditionalEffect } from "react-delta";

function App() {
  const [page, setPage] = useState(1);
  const [search, setSearch] = useState("");

   // traditional way
  useEffect(() => {
    console.log('useEffect: page or search changed')
  }, [page, search])

  // react-delta  way
  const deltas = useDeltaArray([ page, search ]);
  useConditionalEffect(() => {
    // deltas give access to prev and curr values for custom comparison
    console.log('useConditionalEffect: page or search changed', deltas)
  }, some(deltas))

  return (
    <div>
      <input value={search} onChange={e => setSearch(e.target.value)} />
      <button onClick={() => setPage(p => p + 1)}>Next Page: {page}</button>
    </div>
  );
}

Namun pertanyaannya tetap, apakah API ini memberikan terlalu banyak kendali sehingga orang akan menemukan cara untuk menyalahgunakannya?

@ Gaearon Anda mengusulkan solusi yang baik. Namun sayangnya, ini tidak mencakup kasus saat kita meneruskan ke useEffect beberapa objek bersumber pustaka pihak ketiga.
Misalnya, jika kita menggunakan firebase, itu selalu membuat objek Query baru, yang memiliki metode isEqual yang dapat membantu kita jika kita memiliki belas kasihan khusus. Tetapi kami tidak memiliki kemampuan untuk membuat komparator khusus seperti ini:

const areEqual = (queryA, queryB) => queryA.isEqual(queryB)

Anda berkata, bahwa kita dapat menggunakan useMemo di utas ini:

const fetchCollection = (hookName = 'useEffectWithComparator') => {
  const memoizedQuery = useMemo(() => {
     return firebase.firestore().collection('hooks').where('name', '==', hookName), 
  }, [hookName]));
  return useFirestoreQuery(memoizedQuery);
};

const useFirestoreQuery = query => {
   const [state, setState] = useState(null);
   useEffect(() => {
      const result = await query() // we need to wrap it with self-called async function, and so on
      setState(result)
   }, [query])
   return result;
}

Sepertinya berhasil. Tetapi ada catatan penting dari dokumentasi React:

Anda dapat mengandalkan useMemo sebagai pengoptimalan performa, bukan sebagai jaminan semantik . Di masa mendatang, React dapat memilih untuk "melupakan" beberapa nilai yang telah di memo sebelumnya dan menghitung ulang pada render berikutnya, misalnya untuk mengosongkan memori untuk komponen di luar layar. Tulis kode Anda agar tetap berfungsi tanpa useMemo - lalu tambahkan untuk mengoptimalkan kinerja.

Dan yang menakutkan, useMemo ini adalah jaminan semantik . Jika tidak, kode ini akan pergi ke loop tak terbatas.

Bagaimana menurut anda?

@Carduelis Terima kasih telah menanyakan ini, saya telah menggaruk-garuk kepala beberapa saat mencoba mencari tahu apa pendekatan terbaik untuk masalah ini. The useMemo solusi yang @gaearon disajikan _almost_ sempurna bagi kami, kecuali bahwa berita gembira di docs Anda dikutip tentang jaminan semantik membuat saya gugup.

Saya telah melihat jawaban yang mengatakan untuk menggunakan referensi jika Anda memerlukan jaminan semantik, tetapi saya tidak dapat menjelaskan cara menerapkannya ke kasus penggunaan ini.

Contoh saya:

function useFetch({ url, requestBody }){
     const [result, setResult] = useState({ loading: false, data: null, error: null })
     const queryFunc = useCallback(() => {
            // ... do fetch
     }, [url, requestBody]);
     return [result, queryFunc];
}

function MyComponent(props) {
    const body = useMemo(() => ({ id: props.id }), [props.id])
    const [result, query] = useFetch("example", body)

    useEffect(() => {
         query();
    } , [query])
}

Seperti yang Anda lihat, kami menggunakan useMemo untuk menyimpan referensi serta memulai kueri baru setiap kali props.id berubah. Jika useMemo secara acak memutuskan hitung ulang nilainya, ini akan menyebabkan panggilan pengambilan yang tidak terduga. Bagaimana kita bisa menggunakan referensi di sini?

Pertimbangkan paket use-memo-one dengan menyediakan useMemo dan useCallback dengan jaminan semantik tambahan.

Saya telah membuat hook use-custom-bandingkan untuk mengatasi ini. Saya harap ini akan membantu.

Saya membuat aplikasi dengan beberapa pengambilan data dalam @gaearon benar. Mengangkat panggilan Anda dalam modul terpisah adalah jawaban yang benar dalam 9 dari 10 kasus.

Sekarang saya tidak setuju dengan opsi 2. Menggunakan useMemo () untuk mengirimkan variabel instance statis terhadap array dependensi kosong, menurut pendapat saya, adalah pengait yang salah. Coba dulu untuk menentukan mengapa larik ketergantungan Anda harus tetap kosong. Jika memang tidak ada dependensi, coba useRef () sebagai gantinya.

Untuk opsi 3, jangan meneruskan objek / larik JSON.stringify () ke dalam daftar ketergantungan. Sebaliknya, biarkan hook mengenai dan tambahkan kondisional untuk menentukan apakah fungsi bagian dalam harus dilanjutkan.

Dengan asumsi Anda memiliki array objek yang dangkal:

useCallback(() => {
   if (JSON.stringify(myJSONArr) !== JSON.stringify(somethingElse) {
         execFunc()
      }
}, [myJSONArr])

Saya tidak mengerti mengapa react tidak menawarkan opsi untuk melakukan perbandingan yang setara. Setiap orang perlu melakukan pencarian dan membangunnya sendiri, itu hanya membuang-buang waktu. Menjumlahkan waktu bersama semua orang itu banyak.

Saya tidak mengerti mengapa react tidak menawarkan opsi untuk melakukan perbandingan yang setara. Setiap orang perlu melakukan pencarian dan membangunnya sendiri, itu hanya membuang-buang waktu. Menjumlahkan waktu bersama semua orang itu banyak.

Ya, ini cukup mengganggu. Saya memutuskan untuk menggunakan paket objek-hash tampaknya berfungsi dengan baik

Saya tidak mengerti mengapa react tidak menawarkan opsi untuk melakukan perbandingan yang setara. Setiap orang perlu melakukan pencarian dan membangunnya sendiri, itu hanya membuang-buang waktu. Menjumlahkan waktu bersama semua orang itu banyak.

Ya, ini cukup mengganggu. Saya memutuskan untuk menggunakan paket objek-hash tampaknya berfungsi dengan baik

Apakah objek-hash berfungsi untuk objek dengan kunci dalam urutan berbeda?

kemungkinan akan menghasilkan hash baru

Saya sedikit terkejut dengan fakta bahwa React tidak konsisten di sini, karena React.memo memang memiliki ini. Mengapa fungsionalitas ini tersedia di sana jika kait tidak memilikinya?

Saya ingin berbagi masalah yang sering saya hadapi, yang saya percaya akan terpecahkan jika React mendukung fungsi kesetaraan kustom untuk useMemo , useCallback dan useEffect . / cc @gaearon

Masalah

Ambil contoh deriving array.

Contoh lengkap: https://stackblitz.com/edit/react-use-memo-result-yj2wxr?file=index.tsx

console.log(fruits);
// => ['apple', 'banana', 'grapes']

const yellowFruits = useMemo(
  () => fruits.filter((fruit) => fruit === "banana"),
  [fruits]
);
// => ['banana']

Gambar bahwa fruits telah di memo dengan benar. Ini adalah larik yang _hanya mengubah referensi ketika konten di dalamnya berubah_.

Ketika fruits berubah, kami menghitung ulang yellowFruits , yang akan membuat referensi baru.

Ini berarti bahwa kita dapat memiliki referensi baru untuk yellowFruits _bahkan jika isi array tidak berubah_. Contoh:

console.log(fruits);
// => ['apple', 'banana', 'grapes', 'pineapple'] // ✅ new content, new reference

const yellowFruits = useMemo(
  () => fruits.filter((fruit) => fruit === "banana"),
  [fruits]
);
// => ['banana'] // ❌ same content, new reference
  • Jika yellowFruits diteruskan ke hilir sebagai prop ke komponen memo d, kita akan menyia-nyiakan render ulang.
  • Jika diteruskan ke useEffect sebagai ketergantungan, efeknya akan berjalan lebih sering dari yang seharusnya ( seperti yang terlihat pada contoh ).
  • Jika diteruskan ke useMemo atau pilih ulang selektor sebagai ketergantungan, fungsi memoized akan berjalan lebih sering dari yang seharusnya, menyebabkan masalah memoisasi lebih lanjut di hilir.

Anda mengerti.

Larutan

Fungsi kesetaraan kustom untuk useMemo , useCallback dan useEffect akan menyelesaikan masalah ini:

 useEffect(() => {
   console.log("yellowFruits changed");
-}, [yellowFruits]);
+}, [yellowFruits], shallowEqual);

Untuk mendemonstrasikan solusi ini, berikut adalah contoh yang menggunakan use-custom-compare (terima kasih @ kotarella1110!). Tapi idealnya kita tidak perlu menggunakan perpustakaan untuk ini.


Kita sudah bisa memberikan fungsi kesetaraan kustom untuk memilih kembali createSelector dan React React.memo HOC. Mengapa kita ingin memperlakukan pengait ini secara berbeda?

Saat ini kami tidak memiliki rencana untuk melakukan ini. Perbandingan khusus bisa menjadi sangat mahal jika disebarkan ke seluruh komponen - terutama saat orang mencoba melakukan pemeriksaan kesetaraan yang mendalam di sana. Biasanya ada cara berbeda untuk menyusun kode sehingga Anda tidak membutuhkannya.

Ada beberapa solusi umum:

Opsi 1: Angkat

Jika beberapa nilai seharusnya selalu statis, nilai itu tidak perlu ada di fungsi render sama sekali.
Sebagai contoh:

const useFetch = createFetch({ /* static config */});

function MyComponent() {
  const foo = useFetch();
}

Itu benar-benar menyelesaikan masalah. Jika membuat instance menjengkelkan, Anda selalu dapat memiliki modul useFetch Anda sendiri yang hanya mengekspor ulang hasil yang dibuat (atau bahkan menyediakan beberapa variasi):

// ./myUseFetch.js
import createFetch from 'some-library';

export const useFetch = createFetch({ /* config 1 */ })
export const useFetchWithAuth = createFetch({ /* config 2 */ })

Opsi 2: Rangkullah nilai-nilai dinamis

Pilihan lainnya adalah sebaliknya. Sadarilah bahwa nilai-nilai selalu dinamis, dan minta API _consumer_ untuk membuat memo.

function MyComponent() {
  const config = useMemo(() => ({
    foo: 42,
  }, []); // Memoized
  const data = useFetch(url, config);
}

Ini mungkin tampak berbelit-belit. Namun untuk Hook level rendah, Anda mungkin akan membuat pembungkus level yang lebih tinggi. Jadi mereka bisa bertanggung jawab untuk memoisasi yang benar.

// ./myUseFetch.js
import {useContext} from 'react';
import useFetch from 'some-library';
import AuthContext from './auth-context';

export function useFetchWithAuth(url) {
  const authToken = useContext(AuthContext);
  const config = useMemo(() => ({
    foo: 42,
    auth: authToken
  }), [authToken]); // Only changes the config if authToken changes
  return useFetch(url, config);
}

// ./MyComponent.js
function MyComponent() {
  const data = useFethWithAuth('someurl.com'); // No config
}

Kemudian secara efektif pengguna tidak perlu mengonfigurasi apa pun di situs panggilan - atau Anda dapat menampilkan opsi yang didukung terbatas sebagai argumen khusus. Itu mungkin API yang lebih baik daripada objek "opsi" yang sewenang-wenang.

Opsi 3: (hati-hati) JSON.stringify

Banyak orang mengungkapkan keinginannya untuk pemeriksaan kesetaraan yang mendalam. Namun, pemeriksaan kesetaraan yang mendalam sangat buruk karena kinerjanya tidak dapat diprediksi. Mereka bisa menjadi cepat suatu hari, dan kemudian Anda mengubah sedikit struktur datanya, dan mereka menjadi sangat lambat karena kompleksitas traversal telah berubah. Jadi kami ingin secara eksplisit mencegah penggunaan pemeriksaan mendalam - dan useEffect API saat ini melakukannya.

Namun, ada beberapa kasus di mana hal itu terlalu menghalangi ergonomi. Semisal dengan kode like

const variables = {userId: id};
const data = useQuery(variables);

Dalam kasus ini, sebenarnya tidak diperlukan perbandingan mendalam. Dalam contoh ini, kami mengetahui bahwa struktur data kami relatif dangkal, tidak memiliki siklus, dan dapat diserialkan dengan mudah (misalnya karena kami berencana untuk mengirim struktur data tersebut melalui jaringan sebagai permintaan).

Dalam kasus yang jarang terjadi, dapat diterima untuk memberikan [JSON.stringify(variables)] sebagai dependensi.

"Tunggu!" Saya mendengar Anda berkata, "Bukankah JSON.stringify() lambat?"

Pada input kecil, seperti contoh di atas, ini sangat cepat.

Itu menjadi lambat pada input yang lebih besar. Tetapi pada saat itu, kesetaraan yang dalam juga cukup lambat sehingga Anda ingin memikirkan kembali strategi dan API Anda. Seperti kembali ke pembatalan granular dengan useMemo . Itu memberi Anda kinerja - jika memang, Anda peduli tentangnya.

Mungkin ada kasus penggunaan kecil lainnya yang tidak sesuai dengan ini. Untuk kasus penggunaan tersebut, dapat diterima untuk menggunakan ref sebagai jalan keluar. Tetapi Anda harus sangat mempertimbangkan untuk menghindari membandingkan nilai secara manual, dan memikirkan kembali API Anda jika Anda merasa membutuhkannya.

Saya memiliki kasus penggunaan.
misalnya, ada variabel:

const a = [bigObject0, bigObject1, ...manyBigObjects] // length can be change
const b = { bigObject0, bigObject1, bigObject2, ...manyBigObjects } // length can be change
const c = 'I amd just a string'
const d = { a: 1, b: '2' } // length fixed.

React.useEffect(() => {
}, [...a, Object.keys(b).map(key => b[key]), c, d.a, d.b]);

Untuk kasus penggunaan ini, Opsi 3 tidak berfungsi dengan baik, karena saya tidak ingin membuat serial objek dalam. Dan jika a, b tidak dapat dihafal dengan baik, opsi 2 tidak berfungsi juga. Opsi 1 tidak berfungsi dalam banyak situasi. Jadi saya memilih solusi dalam kode. Dan sepertinya bekerja dengan baik. Tapi React selalu menunjukkan peringatannya. React menemukan panjang deps dapat berubah dan itu memberi tahu saya itu buruk. Jadi kenapa buruk?

Saya telah membuat hook use-custom-bandingkan untuk mengatasi ini. Saya harap ini akan membantu.

Beri orang ini medali!

Saya pikir cukup mudah untuk menyusun solusi untuk masalah ini dengan satu atau dua kait kecil, saya kira. Saya baru saja mulai menggunakan hook, jadi mungkin pendekatan saya salah

// Returns a number representing the "version" of current.
function useChangeId<T>(next: T, isEqual: (prev: T | undefined, next: T) => boolean) {
  const prev = useRef<T>()
  const id = useRef(0)
  if (!isEqual(prev.current, next)) {
    id.current++
  }
  useEffect(() => { prev.current = next }, [next])
  return id
}

interface FetchConfig { /* ... etc ... */ }

function useFetch(config:  FetchConfig) {
  // Returns a new ID when config does not deep-equal previous config
  const changeId = useChangeId(config, _.isEqual)
  // Returns a new callback when changeId changes.
  return useCallback(() => implementUseFetchHere(config), [changeId])
}

function MyComponent(props: { url: string }) {
  const fetchConfig = { url, ... }
  const fetch = useFetch(fetchConfig)
  return <button onClick={fetch}>Fetch</button>
}

Apakah halaman ini membantu?
0 / 5 - 0 peringkat