Menu

Hook useCallback w React: kiedy naprawdę pomaga

useCallback zwraca tę samą funkcję między renderowaniami, dopóki nie zmienią się jej zależności. Pomaga tylko wtedy, gdy ta funkcja trafia do dziecka z memo albo jest zależnością efektu. Poznaj składnię, różnicę względem useMemo i błędy, których warto unikać.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

useCallback zapamiętuje funkcję między renderowaniami: zwraca ten sam obiekt funkcji, dopóki nie zmieni się jedna z jej zależności. Ma to znaczenie tylko wtedy, gdy coś porównuje funkcję po referencji, czyli w praktyce dziecko owinięte w memo albo efekt, który ma tę funkcję w zależnościach.

Oba przyciski są zmemoizowane i oba robią to samo. Przełącz motyw i obserwuj konsolę: pojawia się tylko render Inline function, bo saveInline jest nową funkcją przy każdym renderowaniu, więc memo widzi zmieniony prop. saveCached to za każdym razem ta sama funkcja, więc ten przycisk jest pomijany.

Co zapamiętuje

const cachedFn = useCallback(fn, dependencies);
  • fn to funkcja, którą chcesz zachować. React jej nie wywołuje; zwraca ją tobie.
  • dependencies wymienia każdą wartość z komponentu, którą czyta fn. Przy każdym renderowaniu React porównuje je z poprzednimi przez Object.is.
  • Jeśli nic się nie zmieniło, dostajesz funkcję z poprzedniego renderowania. Jeśli coś się zmieniło, dostajesz funkcję, którą przekazałeś tym razem, a React zachowuje ją na następny raz.

Zwróć uwagę, co to oznacza: funkcja strzałkowa wewnątrz useCallback i tak jest tworzona przy każdym renderowaniu, bo to po prostu argument. useCallback nie oszczędza tej pracy. Jego jedynym zadaniem jest oddanie starszej, identycznej funkcji, żeby referencja pozostała ta sama.

useCallback a useMemo

To ten sam mechanizm. useMemo zapamiętuje wynik wywołania funkcji, useCallback zapamiętuje samą funkcję:

// These two lines are equivalent
const handleSave = useCallback(() => save(draft), [draft]);
const handleSave = useMemo(() => () => save(draft), [draft]);

Używaj useCallback dla funkcji, bo lepiej się czyta, a useMemo dla wyliczanych wartości, takich jak przefiltrowana lista albo obiekt opcji.

Pozbywanie się zależności przez funkcje aktualizujące

Zapamiętana funkcja musi wymieniać każdą wartość stanu, którą czyta, a za każdym razem, gdy ten stan się zmienia, funkcja też się zmienia. Przy liście zmemoizowanych wierszy to problem: funkcja remove, która czyta todos, zmienia się przy każdej zmianie todos, więc każdy pozostały wiersz renderuje się ponownie po każdym usunięciu. Użyj formy settera z funkcją aktualizującą, żeby funkcja w ogóle nie czytała todos.

Usuń wiersz: konsola milczy, bo pozostałe wiersze dostały ten sam obiekt todo i tę samą funkcję remove co wcześniej. Zmień funkcję na useCallback((id) => setTodos(todos.filter((t) => t.id !== id)), [todos]) i spróbuj ponownie: teraz każdy pozostały wiersz wypisuje renderowanie po każdym usunięciu.

Funkcje jako zależności efektów

Efekt, który wywołuje funkcję zadeklarowaną w komponencie, musi mieć tę funkcję w zależnościach. Jeśli funkcja jest nowa przy każdym renderowaniu, efekt uruchamia się po każdym renderowaniu. useCallback utrzymuje ją stabilną, dopóki nie zmienią się jej własne dane wejściowe:

Pisanie wiadomości nie łączy ponownie. Zmiana pokoju wypisuje rozłączenie ze starym pokojem i połączenie z nowym. Usuń opakowanie useCallback (zostaw funkcję strzałkową), a każde naciśnięcie klawisza rozłączy i połączy ponownie.

Jest tu jednak prostsze rozwiązanie: przenieś funkcję do wnętrza efektu. Wtedy efekt zależy bezpośrednio od roomId i żadna funkcja nie musi być stabilna.

useEffect(() => {
    function createOptions() {
        return { serverUrl: 'wss://chat.example', roomId };
    }
    const options = createOptions();
    // ...
}, [roomId]);

Wybieraj to, gdy funkcji używa tylko efekt. useCallback zostaw dla funkcji, które też przekazujesz w dół albo zwracasz z własnego hooka, gdzie wywołujący mogą umieścić je we własnych tablicach zależności.

Sprawdzanie, czy funkcja się zmieniła

Gdy dziecko z memo ciągle się renderuje, a ty nie wiesz dlaczego, sprawdź, czy prop z funkcją to naprawdę ten sam obiekt między renderowaniami. Trzymaj poprzednią funkcję w refie i porównuj:

Kliknij Count: konsola mówi increment changed: false, bo step się nie zmienił. Kliknij Step, a powie true: step jest zależnością, więc React oddał nową funkcję. Funkcja, która zmienia się przy każdym renderowaniu, zwykle oznacza, że któraś zależność sama jest nowa przy każdym renderowaniu, na przykład obiekt albo inna funkcja zadeklarowana w komponencie.

useCallback we własnych hookach

Własny hook, który zwraca funkcje, to dobre miejsce na useCallback, nawet gdy nie wiesz, kto będzie go wywoływał. Komponent używający hooka może przekazać te funkcje dziecku z memo albo umieścić je w efekcie i nie może ustabilizować ich z zewnątrz. Owinięcie ich wewnątrz hooka daje każdemu wywołującemu domyślnie stabilną referencję:

function useCounter(initial = 0) {
    const [count, setCount] = useState(initial);
    const increment = useCallback(() => setCount((c) => c + 1), []);
    const reset = useCallback(() => setCount(initial), [initial]);
    return { count, increment, reset };
}

Częsty błąd: owijanie wszystkiego

useCallback wokół handlera, który trafia prosto do <button> albo <input>, nie robi nic pożytecznego. React nie pomija żadnej pracy dla elementu DOM dlatego, że jego handler pozostał ten sam, a komponent, który nie jest owinięty w memo, i tak renderuje się, gdy renderuje się jego rodzic.

// Pointless: <button> does not care that the function is the same
const handleClick = useCallback(() => setOpen(true), []);
return <button onClick={handleClick}>Open</button>;

Każde dodatkowe useCallback dodaje tablicę zależności, w której ktoś może się pomylić, a nieaktualna zależność to prawdziwy błąd (funkcja dalej czyta stary stan). Dodawaj go, gdy po stronie odbiorcy jest dziecko z memo albo tablica zależności, a nie domyślnie.

React Compiler 1.0, gdy twój build go używa, automatycznie memoizuje funkcje i wartości, co eliminuje potrzebę większości ręcznie pisanych wywołań useCallback. Bez kompilatora powyższe zasady nadal obowiązują, a ręczne useCallback wciąż jest powszechne w istniejącym kodzie.

Najczęściej zadawane pytania

Co robi useCallback?

Zwraca ten sam obiekt funkcji przy każdym renderowaniu, dopóki nie zmieni się jedna z jej zależności. Nie powstrzymuje tworzenia funkcji; zamiast nowej oddaje funkcję z wcześniejszego renderowania.

Kiedy używać useCallback?

Gdy przekazujesz funkcję do dziecka owiniętego w memo albo gdy funkcja jest zależnością efektu lub innego hooka. W obu przypadkach nowa funkcja przy każdym renderowaniu unieważniłaby optymalizację albo ponownie uruchamiała efekt.

Czym różni się useCallback od useMemo?

useCallback(fn, deps) zapamiętuje samo fn. useMemo(() => value, deps) zapamiętuje wartość, którą zwraca funkcja. useCallback(fn, deps) to dokładnie useMemo(() => fn, deps).

Czy owijać każdą funkcję w useCallback?

Nie. Jeśli funkcja trafia do zwykłego elementu DOM albo do komponentu, który nie jest zmemoizowany, stabilna referencja niczego nie zmienia, a dodatkowe tablice zależności utrudniają czytanie kodu i ułatwiają pomyłki.

Czy useCallback przyspiesza mój komponent?

Sam z siebie nie. Pomaga tylko wtedy, gdy coś dalej porównuje funkcję po referencji, na przykład memo albo tablica zależności efektu.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ