useCallback
useCallback
jest hookiem reactowym, który pozwala na zapamiętywanie (ang. cache, memoize) definicji funkcji pomiędzy przerenderowaniami.
const cachedFn = useCallback(fn, dependencies)
Dokumentacja
useCallback(fn, dependencies)
Wywołaj useCallback
na głównym poziomie komponentu, aby zapamiętać definicje funkcji pomiędzy przerenderowaniami:
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
Więcej przykładów znajdziesz poniżej.
Parametry
-
fn
: Funkcja, którą chcesz zapamiętać. Może przyjąć dowolne argumenty i zwrócić dowolne wartości. React zwróci (nie wywoła!) twoją funkcję z powrotem w pierwszym renderowaniu. Przy kolejnych renderowaniach, React zwróci ci tę samą funkcję ponownie jeśli lista zależnościdependencies
nie zmieni się od ostatniego renderowania. W przeciwnym razie zwróci ci funkcję, którą przekazałeś podczas obecnego renderowania i zachowa ją do ponownego użycia potem. React nie wywoła twojej funkcji. Funkcja ta zostanie ci zwrócona, abyś mógł sam wybrać gdzie i kiedy ma być wywołana. -
dependencies
: Lista wszystkich reaktywnych wartości użytych w kodzie funkcjifn
. Reaktywne wartości to właściwości, stan i wszystkie inne zmienne i funkcje zadeklarowane bezpośrednio wewnątrz ciała komponentu. Jeżeli twój linter jest skonfigurowany pod Reacta, sprawdzi on czy każda reaktywna wartość jest poprawnie wskazana jako zależność. Lista zależności musi mieć stałą liczbę elementów i byś zapisana wprost jak np.[dep1, dep2, dep3]
. React porówna każdą zależność z jej poprzednią wartością używając algorytmu porównaniaObject.is
.
Zwracana wartość
Podczas pierwszego renderowania, useCallback
zwróci funkcję fn
, która została mu przekazana.
Podczas kolejnych renderowań, zwróci on już zapamiętaną funkcję fn
z poprzedniego renderowania (jeśli zależności nie uległy zmianie) albo zwróci funkcję fn
, którą przekazałeś podczas tego renderowania.
Zastrzeżenia
useCallback
jest hookiem, więc można go wywoływać tylko na głównym poziomie komponentu lub innego hooka. Nie można go wywołać w pętli lub instrukcji warunkowej. Jeśli masz sytuację, która wymaga pętli lub warunku, stwórz nowy komponent i przenieś do niego stan.- React nie odrzuci zapamiętanej funkcji, chyba że istnieje konkretny powód ku temu. Na przykład, w środowisku developerskim React odrzuca zapamiętaną funkcję, gdy komponent jest edytowany. Zarówno w środowisku developerskim jak i w produkcji React odrzuci zapamiętaną funkcję jeśli twój komponent zostaje zawieszony podczas pierwszego montowania. W przyszłości, React może dodać więcej funkcjonalności, które skorzystają z odrzucania zapamiętanej funkcji - na przykład, jeśli React doda w przyszłości wsparcie dla zwirtualizowanych list, będzie to miało sens, aby odrzucić zapamiętane funkcje dla elementów, które wyszły poza widoczny obszar zwirtualizowanej tablicy. To powinno sprostać twoim oczekiwaniom jeżeli polegasz na
useCallback
jako optymalizacji wydajności. W innym przypadku, zmienna stanu lub referencja może być lepsza.
Sposób użycia
Pomijanie przerenderowywania komponentów
Gdy optymalizujesz wydajność renderowania, czasem zachodzi potrzeba zapamiętania funkcji, którą przekazujesz do potomków. Spójrzmy najpierw na składnię jak to zrobić, a potem w jakich przypadkach jest to przydatne.
Aby zapamiętać funkcję pomiędzy renderowaniami twojego komponentu, zawrzyj jej definicję w hooku useCallback
:
import { useCallback } from 'react';
function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
// ...
Musisz przekazać dwie rzeczy do useCallback
:
- Definicję funkcji, którą chcesz zapamiętać pomiędzy przerenderowaniami.
- Listę zależności zawierającą wszystkie wartości wewnątrz komponentu, które są użyte w twojej funkcji.
Przy pierwszym renderowaniu, zwrócona funkcja otrzymana z useCallback
będzie tą samą, którą przekazałeś.
Przy następnych renderowaniach, React porówna zależności z zależnościami, które przekazałeś w poprzednim renderowaniu. Jeśli żadna z zależności nie uległa zmianie (porównując z użyciem Object.is
), useCallback
zwróci tę samą funkcję co poprzednio. W innym wypadku, useCallback
zwróci funkcję, którą przekazałeś w tym renderowaniu.
Innymi słowy, useCallback
zapamięta funkcję pomiędzy przerenderowaniami dopóki zależności się nie zmienią.
Posłużmy się przykładem, aby zobaczyć kiedy to może być przydatne.
Załóżmy, że przekazujesz funkcję handleSubmit
w dół z ProductPage
do komponentu ShippingForm
:
function ProductPage({ productId, referrer, theme }) {
// ...
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
Zauważyłeś, że przełączanie właściwości theme
blokuje na chwilę aplikację, ale gdy usuniesz <ShippingForm />
z twojego JSX, zauważysz, że znów działa gładko. To pokazuje, że warto jest spróbować zoptymalizować komponent ShippingForm
.
Domyślnie, gdy komponent jest ponownie przerenderowywany, React także przerenderowuje rekursywnie wszystkich jego potomków. Dlatego też, gdy ProductPage
zostaje przerenderowany z innym theme
, komponent ShippingForm
również zostaje przerenderowany. Jest to akceptowalne dla komponentów, które nie wymagają dużo obliczeń do przerenderowania. Ale jeśli upewniłeś się, że przerenderowanie trwa długo, można wskazać komponentowi ShippingForm
, aby pominął przerenderowanie, gdy jego właściwości są takie same jak podczas ostatniego przerenderowania, poprzez opakowanie go w memo
:
import { memo } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});
Po dokonaniu tej zmiany, komponent ShippingForm
pominie przerenderowanie, jeśli wszystkie jego właściwości są takie same jak podczas ostatniego renderowania. To jest moment, w którym istotne staje się zapamiętywanie funkcji! Załóżmy, że zdefiniowałeś funkcję handleSubmit
bez użycia hooka useCallback
:
function ProductPage({ productId, referrer, theme }) {
// Za każdym razem, gdy zmienia się theme, to będzie inna funkcja...
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
{/* ... więc właściwości ShippingForm nigdy nie będą takie same i komponent ten przerenderuje się za każdym razem */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
W języku JavaScript, function () {}
lub () => {}
zawsze tworzy inną funkcję, podobnie jak literał obiektu {}
zawsze tworzy nowy obiekt. Zazwyczaj nie byłoby to problemem, ale oznacza to, że właściwości komponentu ShippingForm
nigdy nie będą takie same i optymalizacja memo
nie zadziała. Tutaj useCallback
staje się pomocny:
function ProductPage({ productId, referrer, theme }) {
// Powiedz Reactowi, aby zapamiętał twoją funkcję między przerenderowaniami...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ... dopóki te zależności się nie zmienią...
return (
<div className={theme}>
{/* ...ShippingForm otrzyma tych samych potomków i może pominąć przerenderowanie */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
Opakowanie handleSubmit
w useCallback
zapewnia, że to jest ta sama funkcja między przerenderowaniami (aż do zmiany zależności). Nie musisz opakowywać funkcji w useCallback
, chyba że robisz to z jakiegoś konkretnego powodu. W tym przykładzie powodem jest to, że przekazujesz ją do komponentu opakowanego w memo
, co pozwala na pominięcie przerenderowania. Istnieją inne powody, dla których możesz potrzebować useCallback
, opisane dalej na tej stronie.
Dla dociekliwych
Często zobaczysz useMemo
obok useCallback
. Oba są przydatne, gdy próbujesz zoptymalizować komponent potomny. Pozwalają one na memoizację (lub inaczej mówiąc, zapamiętywanie) tego, co przekazujesz w dół:
import { useMemo, useCallback } from 'react';
function ProductPage({ productId, referrer }) {
const product = useData('/product/' + productId);
const requirements = useMemo(() => { // Wywołuje twoją funkcję i zapamiętuje jej wynik
return computeRequirements(product);
}, [product]);
const handleSubmit = useCallback((orderDetails) => { // Zapamiętuje samą twoją funkcję
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
<ShippingForm requirements={requirements} onSubmit={handleSubmit} />
</div>
);
}
Różnica polega na tym co pozwalają ci zapamiętać:
useMemo
zapamiętuje wynik wywołania twojej funkcji. W tym przykładzie jest to wynik wywołaniacomputeRequirements(product)
, aby nie zmieniał się, chyba że zmieni sięproduct
. Pozwala to przekazywać obiektrequirements
w dół bez niepotrzebnego ponownego przerenderowaniaShippingForm
. Gdy będzie to konieczne, React wywoła funkcję, którą przekazałeś podczas renderowania, aby obliczyć wynik.useCallback
zapamiętuje samą funkcję. W przeciwieństwie douseMemo
, nie wywołuje dostarczonej funkcji. Zamiast tego zapamiętuje funkcję, którą podałeś, tak abyhandleSubmit
sam nie zmieniał się, chyba że zmieni sięproductId
lubreferrer
. Pozwala to przekazywać funkcjęhandleSubmit
w dół bez niepotrzebnego ponownego przerenderowaniaShippingForm
. Twój kod nie zostanie uruchomiony, dopóki użytkownik nie prześle formularza.
Jeśli już jesteś zaznajomiony z useMemo
, pomocne może być myślenie o useCallback
w ten sposób:
// Uproszczona implementacja (wewnątrz Reacta)
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}
Dla dociekliwych
Jeśli twoja aplikacja jest podobna do tej strony i większość interakcji jest prostych (takich jak zastępowanie strony lub całej sekcji), to zazwyczaj zapamiętywanie nie jest konieczne. Z drugiej strony, jeśli twoja aplikacja przypomina edytor rysunków i większość interakcji jest dość szczegółowa (takie jak przesuwanie kształtów), to możliwe, że zapamiętywanie będzie bardzo pomocne.
Zapamiętywanie funkcji za pomocą useCallback
daje wyraźne korzyści tylko w kilku przypadkach:
- Przekazujesz ją jako właściwość do potomka, który jest owinięty w
memo
. Chcesz pominąć przerenderowanie, jeśli wartość się nie zmieniła. Zapamiętywanie pozwala komponentowi przerenderować się tylko wtedy, gdy zmienią się zależności. - Funkcja, którą przekazujesz, jest później używana jako zależność jakiegoś Hooka. Na przykład inna funkcja owinięta w
useCallback
zależy od niej lub ty zależysz od tej funkcji w hookuuseEffect.
W innych przypadkach nie ma korzyści z owijania funkcji w useCallback
. Nie ma to również znaczącego wpływu na działanie, więc niektóre zespoły wybierają, by nie zastanawiać się nad indywidualnymi przypadkami i stosować zapamiętywanie tak często, jak to możliwe. Wadą tego podejścia jest jednak to, że kod staje się mniej czytelny. Dodatkowo, nie zawsze zapamiętywanie jest skuteczne: pojedyncza wartość, która “zawsze jest nowa”, może wystarczyć, aby zepsuć zapamiętywanie dla całego komponentu.
Należy zaznaczyć, że useCallback
nie zapobiega tworzeniu funkcji. Zawsze tworzysz funkcję (i nie ma w tym nic złego!), ale React ją ignoruje i zwraca zapamiętaną funkcję, jeśli nic się nie zmieniło.
W praktyce możesz uniknąć wielu przypadków zapamiętywania, stosując kilka zasad:
- Gdy komponent wizualnie zawiera inne komponenty, pozwól mu przyjmować JSX jako komponenty potomne. Wtedy, jeśli komponent wrapujący aktualizuje swój własny stan, React wie, że jego komponenty potomne nie muszą być przerenderowane.
- Preferuj stan lokalny i nie wynoś stanu wyżej niż to jest konieczne. Nie przechowuj chwilowego stanu, takiego jak formularze czy informacji o tym, czy element został najechany kursorem, na samej górze drzewa komponentów lub w bibliotece globalnego stanu.
- Utrzymuj swoją logikę renderowania czystą. Jeśli przerenderowanie komponentu powoduje problem lub widoczne wizualne artefakty, to jest błąd w twoim komponencie! Napraw go zamiast dodawać zapamiętywanie.
- Unikaj niepotrzebnych efektów, które aktualizują stan. Większość problemów wydajnościowych w aplikacjach reactowych wynika z serii aktualizacji, które mają swoje źródło w efektach i prowadzą do wielokrotnego przerenderowania komponentów.
- Postaraj się usunąć niepotrzebne zależności z efektów. Na przykład zamiast zapamiętywania, często prostsze jest przeniesienie jakiegoś obiektu lub funkcji do efektu lub na zewnątrz komponentu.
Jeśli jakaś interakcja wciąż działa opornie, użyj narzędzi do profilowania w narzędziach deweloperskich Reacta, aby zobaczyć, które komponenty najwięcej zyskują na zapamiętywaniu i dodaj zapamiętywanie tam, gdzie jest to potrzebne. Te zasady sprawiają, że twoje komponenty są łatwiejsze do debugowania i zrozumienia, więc warto się nimi kierować w każdym przypadku. Długoterminowo pracujemy nad automatycznym zapamiętywaniem, aby rozwiązać ten problem raz na zawsze.
Przykład 1 z 2: Pomijanie przerenderowania za pomocą useCallback
i memo
W tym przykładzie komponent ShippingForm
jest sztucznie spowolniony, abyś mógł zobaczyć, co się dzieje, gdy komponent, który renderujesz, jest naprawdę wolny. Spróbuj zwiększyć licznik i przełączyć motyw.
Zwiększanie licznika wydaje się wolne, ponieważ wymusza przerenderowanie spowolnionego komponentu ShippingForm
. Jest to oczekiwane, ponieważ licznik się zmienił, więc musisz odzwierciedlić nowy wybór użytkownika na ekranie.
Następnie spróbuj przełączyć motyw. Dzięki useCallback
razem z memo
, jest to szybkie pomimo sztucznego spowolnienia! ShippingForm
pominął przerenderowanie, ponieważ funkcja handleSubmit
nie zmieniła się. Funkcja handleSubmit
nie zmieniła się, ponieważ ani productId
, ani referrer
(twoje zależności w useCallback
) nie zmieniły się od ostatniego przerenderowania.
import { useCallback } from 'react'; import ShippingForm from './ShippingForm.js'; export default function ProductPage({ productId, referrer, theme }) { const handleSubmit = useCallback((orderDetails) => { post('/product/' + productId + '/buy', { referrer, orderDetails, }); }, [productId, referrer]); return ( <div className={theme}> <ShippingForm onSubmit={handleSubmit} /> </div> ); } function post(url, data) { // Załóżmy, że to wysyła zapytanie... console.log('POST /' + url); console.log(data); }
Aktualizacja stanu z zapamiętanej funkcji zwrotnej (ang. callback)
Czasami może być konieczne zaktualizowanie stanu na podstawie poprzedniego stanu z zapamiętanej funkcji zwrotnej.
Funkcja handleAddTodo
posiada todos
jako zależność, ponieważ oblicza następne zadania na jej podstawie:
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...
Zazwyczaj powinieneś dążyć do tego, aby zapamiętane funkcje miały jak najmniej zależności. Gdy odczytujesz pewien stan tylko po to, aby obliczyć jego następną wartość, możesz usunąć tę zależność, przekazując zamiast tego funkcję aktualizującą:
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // ✅ Nie ma potrzeby, aby todos było zależnością
// ...
W tym przypadku zamiast robienia z todos
zależność i odczytywania go wewnątrz funkcji, przekazujesz do Reacta instrukcję, jak aktualizować stan (todos => [...todos, newTodo]
). Dowiedz się więcej o funkcjach aktualizujących.
Zapobieganie zbyt częstemu wyzwalaniu efektu
Czasami może zdarzyć się, że chcesz wywołać funkcję wewnątrz efektu:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
// ...
To powoduje pewien problem. Każda reaktywna wartość musi być zadeklarowana jako zależność twojego efektu. Jednak jeśli zadeklarujesz createOptions
jako zależność, spowoduje to, że twój efekt będzie ciągle ponawiał łączenie się z pokojem czatowym:
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 Problem: Ta zależność zmienia się z każdym renderowaniem
// ...
Aby to rozwiązać, możesz opakować funkcję, którą musisz wywołać z efektu, za pomocą useCallback
:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ Zmienia się tylko wtedy, gdy zmienia się roomId
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // ✅ Zmienia się tylko wtedy, gdy zmienia się createOptions
// ...
To zapewnia, że funkcja createOptions
pozostaje taka sama między przerenderowaniami, jeśli roomId
jest taki sam. Jednakże jeszcze lepiej jest usunąć potrzebę zależności funkcji. Przenieś swoją funkcję do wnętrza efektu:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() { // ✅ Nie ma potrzeby użycia useCallback ani zależności od funkcji!
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Zmienia się tylko wtedy, gdy zmienia się roomId
// ...
Teraz twój kod jest prostszy i nie wymaga użycia useCallback
. Dowiedz się więcej o usuwaniu zależności efektu.
Optymalizacja własnego hooka
Jeśli piszesz własny hook, zaleca się, aby owijać dowolne funkcje, które zwraca, za pomocą useCallback
:
function useRouter() {
const { dispatch } = useContext(RouterStateContext);
const navigate = useCallback((url) => {
dispatch({ type: 'navigate', url });
}, [dispatch]);
const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);
return {
navigate,
goBack,
};
}
To zapewnia, że konsumenci twojego hooka mogą zoptymalizować swój własny kod, gdy jest to potrzebne.
Znane problemy
Za każdym razem, gdy mój komponent renderuje się, useCallback
zwraca inną funkcję
Upewnij się, że podałeś tablicę zależności jako drugi argument!
Jeśli zapomnisz o tablicy zależności, useCallback
będzie zwracać nową funkcję za każdym razem:
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}); // 🔴 Zwraca nową funkcję za każdym razem: brak tablicy zależności
// ...
To jest poprawiona wersja, w której przekazujesz tablicę zależności jako drugi argument:
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ✅ Nie zwraca niepotrzebnie nowej funkcji
// ...
Jeśli to nie pomaga, problem może wynikać z tego, że przynajmniej jedna z twoich zależności zmieniła się od poprzedniego renderowania. Możesz rozwiązać ten problem, dodając ręcznie logowanie twoich zależności do konsoli:
const handleSubmit = useCallback((orderDetails) => {
// ..
}, [productId, referrer]);
console.log([productId, referrer]);
Następnie w konsoli kliknij prawym przyciskiem myszy na tablicach z różnych przerenderowań i wybierz “Zachowaj jako zmienną globalną” (ang. Save as global variable) dla obu z nich. Zakładając, że pierwsza została zapisana jako temp1
, a druga jako temp2
, możesz następnie użyć konsoli przeglądarki, aby sprawdzić, czy każda zależność w obu tablicach jest taka sama:
Object.is(temp1[0], temp2[0]); // Czy pierwsza zależność jest taka sama między tablicami?
Object.is(temp1[1], temp2[1]); // Czy druga zależność jest taka sama między tablicami?
Object.is(temp1[2], temp2[2]); // ... i tak dalej dla każdej zależności ...
Kiedy znajdziesz, która zależność psuje zapamiętywanie, albo znajdź sposób, aby ją usunąć, albo również ją zapamiętaj.
Muszę użyć useCallback
dla każdego elementu listy w pętli, ale nie jest to dozwolone
Załóżmy, że komponent Chart
jest owinięty w memo
. Chcesz uniknąć przerenderowania każdego komponentu Chart
na liście, gdy komponent ReportList
zostanie ponownie przerenderowany. Jednak nie możesz wywołać useCallback
w pętli:
function ReportList({ items }) {
return (
<article>
{items.map(item => {
// 🔴 Nie możesz wywołać useCallback w pętli w ten sposób:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure key={item.id}>
<Chart onClick={handleClick} />
</figure>
);
})}
</article>
);
}
Zamiast tego wyodrębnij komponent dla pojedynczego elementu i umieść w nim useCallback
:
function ReportList({ items }) {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
}
function Report({ item }) {
// ✅ Wywołaj useCallback na najwyższym poziomie:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
}
Ewentualnie, możesz usunąć useCallback
z ostatniego fragmentu kodu i zamiast tego owinąć sam komponent Report
w memo
. Jeśli właściwość item
się nie zmienia, komponent Report
pominie przerenderowanie, a zatem komponent Chart
również pominie przerenderowanie:
function ReportList({ items }) {
// ...
}
const Report = memo(function Report({ item }) {
function handleClick() {
sendReport(item);
}
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
});