useMemo zapamiętuje wynik obliczenia między renderowaniami. React uruchamia twoją funkcję przy pierwszym renderowaniu, zachowuje wynik i przy kolejnych renderowaniach oddaje ten sam wynik, dopóki nie zmieni się jedna z wartości w tablicy zależności.
Otwórz konsolę i kilka razy przełącz motyw: nic się nie wypisuje, a przełączenie wydaje się natychmiastowe. Zmień kartę, a pojawi się filtering for active, z krótką pauzą, bo tab jest zależnością. Teraz zastąp linię z useMemo przez const visible = filterTodos(todos, tab); i znowu przełącz motyw: każde kliknięcie wypisuje log i zacina się na 100 ms.
Składnia
const cachedValue = useMemo(calculateValue, dependencies);
const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);
calculateValueto funkcja bez argumentów, która zwraca wartość do zapamiętania. React wywołuje ją podczas renderowania.dependenciesto lista każdej wartości z komponentu, którą czyta funkcja: propsów, stanu i zmiennych zadeklarowanych w ciele komponentu.- Przy każdym renderowaniu React porównuje każdą zależność z jej wartością z poprzedniego renderowania przez
Object.is. Jeśli wszystkie się zgadzają, zwraca zapamiętaną wartość bez wywoływania twojej funkcji. Jeśli którakolwiek się różni, wywołuje funkcję i zapamiętuje nowy wynik.
useMemo to hook, więc przestrzega zasad hooków: wywołuj go na najwyższym poziomie komponentu, nie w pętli ani warunku.
Zależności muszą być kompletne
Pomiń wartość w tablicy, a cache będzie podawał wynik wyliczony z jej starej wartości. Tutaj suma czyta qty, ale na liście jest tylko price:
Kliknij Quantity: zmienia się na 2, ale suma zostaje na 10 i nic się nie wypisuje. Kliknij Price, a suma skoczy do 22, bo dopiero wtedy obliczenie wykonuje się ponownie, teraz z bieżącym qty. Dodaj qty do tablicy, a oba przyciski będą aktualizować sumę. W prawdziwym projekcie ten błąd wychwytuje reguła lintera react-hooks/exhaustive-deps.
Odwrotny błąd to zależność, która jest nowa przy każdym renderowaniu, na przykład obiekt utworzony w ciele komponentu. Nigdy nie zgadza się z poprzednią wartością, więc cache nigdy nie jest używany:
const filters = { tab, query }; // a new object every render
const visible = useMemo(() => applyFilters(todos, filters), [todos, filters]); // recomputes every time
// List the primitive values instead
const visible = useMemo(() => applyFilters(todos, { tab, query }), [todos, tab, query]);
Stabilny obiekt dla dziecka z memo
Drugi powód, żeby sięgnąć po useMemo, nie ma nic wspólnego z wolnymi obliczeniami. Dziecko owinięte w memo pomija renderowanie tylko wtedy, gdy każdy prop jest taki sam jak ostatnio. Literał obiektu zapisany w ciele komponentu to nowy obiekt przy każdym renderowaniu, więc unieważnia memo, nawet gdy jego zawartość się nie zmieniła. useMemo zachowuje ten sam obiekt, dopóki nie zmienią się jego dane wejściowe.
Pisz w polu notatki, a Chart milczy, podczas gdy Switch unit wypisuje jedno renderowanie. Zastąp wywołanie useMemo zwykłym const options = { unit, max: unit === 'km' ? 100 : 62 };, a każde naciśnięcie klawisza w notatce wypisze render Chart.
To samo dotyczy efektu, który zależy od obiektu. Jeśli efekt ma options w zależnościach, a options jest budowane od nowa przy każdym renderowaniu, efekt uruchamia się po każdym renderowaniu. useMemo to naprawia, ale prostszym rozwiązaniem jest często utworzenie obiektu wewnątrz efektu i uzależnienie go od wartości prymitywnych:
useEffect(() => {
const options = { serverUrl, roomId };
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);
Kiedy nie używać useMemo
Większość obliczeń w komponencie jest tania: dodawanie liczb, łączenie dwóch tekstów, filtrowanie kilkudziesięciu elementów. Zapamiętywanie ich prawie nic nie oszczędza: React i tak przechowuje wartość i porównuje zależności przy każdym renderowaniu, a kod staje się trudniejszy do czytania.
// No useMemo needed
const fullName = first + ' ' + last;
const doneCount = todos.filter((t) => t.done).length;
Żeby sprawdzić, czy coś jest wolne, zmierz czas. Owiń obliczenie w console.time('filter') i console.timeEnd('filter'), wykonaj interakcję i spójrz na liczbę. W przybliżeniu kandydatem jest praca, która na twoim komputerze trwa milisekundę albo więcej, pamiętając, że urządzenia twoich użytkowników mogą być wolniejsze. Zanim sięgniesz po useMemo, sprawdź też, czy ta praca w ogóle należy do renderowania: dane, które nigdy się nie zmieniają, można wyliczyć raz poza komponentem, a stan, którego używa tylko jedna mała część strony, można przenieść do mniejszego komponentu (zobacz renderowanie).
useMemo a useEffect dla wartości pochodnych
Częsty wzorzec to wyliczanie wartości w efekcie i zapisywanie jej w stanie:
// Two renders per change, and the first one has stale data
const [visible, setVisible] = useState([]);
useEffect(() => {
setVisible(filterTodos(todos, tab));
}, [todos, tab]);
// One render, always in sync
const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);
Wersja z efektem renderuje się raz ze starym visible, potem uruchamia efekt, ustawia stan i renderuje ponownie. To dodatkowe renderowanie, a w pierwszym z nich lista nie pasuje do karty. Wyliczanie podczas renderowania z useMemo (albo całkiem bez hooka, jeśli praca jest tania) daje właściwą wartość w tym samym renderowaniu. Efekty służą do synchronizacji z rzeczami spoza Reacta, a nie do wyprowadzania jednego kawałka stanu z innego.
Wskazówka wydajnościowa, a nie gwarancja
useMemo mówi Reactowi, że może ponownie użyć wyniku. Nie obiecuje, że to zrobi. React przy normalnym działaniu zachowuje zapamiętaną wartość, ale może ją wyrzucić, na przykład gdy edytujesz plik w trakcie pracy albo gdy komponent zawiesza się przy pierwszym montowaniu. Pisz kod tak, żeby był poprawny także wtedy, gdy obliczenie wykona się ponownie: funkcja musi być czysta, bez efektów ubocznych, takich jak żądania, subskrypcje czy zmienianie zmiennych poza nią. Efekty uboczne należą do handlera zdarzenia albo efektu.
W trybie deweloperskim pod StrictMode React wywołuje twoją funkcję obliczającą dwa razy, żeby pomóc ci wychwycić nieczysty kod. Podgląd tutaj działa jak build produkcyjny, więc każdy log filtering for zobaczysz raz.
useMemo i React Compiler
React Compiler 1.0 to narzędzie budowania, które analizuje twoje komponenty i automatycznie dodaje memoizację, zapamiętując obliczenia i JSX w miejscach, w których pomogłyby useMemo, useCallback i memo. W projekcie, który go używa, napiszesz ich ręcznie mniej. Ręczne useMemo nadal działa obok kompilatora, nadal jest narzędziem, gdy kompilator nie jest skonfigurowany, i spotkasz je w większości istniejącego kodu w React.
Najczęściej zadawane pytania
Co robi useMemo w React?
Wywołuje twoją funkcję obliczającą przy pierwszym renderowaniu, zapamiętuje wynik, a przy kolejnych renderowaniach zwraca zapamiętany wynik, dopóki każda wartość w tablicy zależności jest taka sama jak ostatnio (porównywana przez Object.is).
Kiedy używać useMemo?
Gdy obliczenie jest zauważalnie wolne, a jego dane wejściowe rzadko się zmieniają, albo gdy obiekt lub tablica musi zachować tę samą referencję między renderowaniami, bo zależy od niej dziecko z memo albo efekt. Przy taniej pracy go pomiń.
Czym różni się useMemo od useCallback?
useMemo zapamiętuje wartość, którą zwraca twoja funkcja. useCallback zapamiętuje samą funkcję. useCallback(fn, deps) to to samo co useMemo(() => fn, deps).
Czy useMemo gwarantuje, że wartość nigdy nie zostanie przeliczona?
Nie. To wskazówka wydajnościowa. React w niektórych przypadkach może wyrzucić zapamiętaną wartość, na przykład w trybie deweloperskim, gdy edytujesz plik, więc twój kod musi działać poprawnie także wtedy, gdy obliczenie wykona się ponownie.
Czy z React Compiler nadal potrzebuję useMemo?
React Compiler może dodawać memoizację automatycznie, gdy twój build go używa. Bez kompilatora albo w przypadkach, które pomija, ręczne useMemo działa tak samo i nadal jest powszechne w istniejącym kodzie.