Komponent czatu łączy się ponownie przez WebSocket za każdym razem, gdy wpisujesz literę w polu wyszukiwania, które nie ma nic wspólnego z czatem. Inny komponent renderuje się w kółko, aż karta się zawiesza. Trzeci wywołuje swój useEffect dwa razy przy montowaniu, ale tylko w trybie deweloperskim. Żaden z tych przypadków nie jest błędem Reacta. Dwa pierwsze to tablica zależności robiąca dokładnie to, co jej kazano, a trzeci to Strict Mode sprawdzający, czy twój efekt umie się po sobie posprzątać.
Kiedy używać useEffect w komponencie React
useEffect synchronizuje komponent z czymś, co istnieje poza Reactem: połączeniem z serwerem, API przeglądarki, timerem, bezpośrednio z DOM-em, widgetem z zewnątrz. React renderuje twój JSX, zatwierdza go w DOM-ie, a potem wykonuje twoje efekty. Gdy render nie został wywołany interakcją taką jak kliknięcie, React zwykle najpierw pozwala przeglądarce namalować nowy ekran.
Efekt jest więc złym miejscem na liczenie wartości potrzebnej do renderu. Jeśli fullName można wyprowadzić z firstName i lastName, zapisz to podczas renderu:
| |
Nie jako efekt, który wywołuje setFullName po fakcie. Wersja z efektem renderuje się raz z nieaktualną wartością, planuje aktualizację stanu, potem renderuje się znowu. Ten dodatkowy przebieg nic nie daje. Sięgaj po useEffect, gdy komponent musi zrobić coś, bo został wyświetlony, a nie żeby policzyć coś, co mógłby policzyć bezpośrednio.
Jak działa tablica zależności useEffect
Drugi argument useEffect przychodzi w trzech formach, i każda mówi Reactowi coś innego o tym, kiedy ponownie wykonać funkcję.
| |
Dla wypełnionej tablicy React porównuje każdy element z jego wartością z poprzedniego renderu za pomocą Object.is. To ta sama kontrola co ===, z tą różnicą, że NaN jest równe samemu sobie, a 0 i -0 są różne. Jeśli każda zależność jest identyczna jak ostatnio, React pomija efekt. Jeśli jedna się różni, wykonuje go ponownie. Nie ma częściowego dopasowania i nie ma sposobu, by powiedzieć Reactowi “wykonaj ponownie tylko wtedy, gdy zmieni się właśnie ta jedna zależność”. Ta logika musi znaleźć się wewnątrz efektu.
To też znaczy, że nie możesz po prostu wyrzucić wartości z tablicy, żeby powstrzymać efekt od ponownego wykonania. Tablica powinna wymieniać każdą reaktywną wartość, którą czyta ciało funkcji: stan, propsy i wszystko, co z nich wynika. Usuń wartość z tablicy, ale dalej ją czytaj, i efekt będzie działał na nieaktualnej kopii. Reguła react-hooks/exhaustive-deps z eslint-plugin-react-hooks istnieje właśnie po to, by wyłapać tę niezgodność. Traktuj jej ostrzeżenia jak błąd builda, nie uciszaj ich komentarzem wyłączającym.
Co robi funkcja cleanup w useEffect
Efekt może zwrócić funkcję. React traktuje tę zwracaną wartość jako cleanup dla tego, co efekt uruchomił:
| |
React wywołuje cleanup w dwóch sytuacjach: chwilę przed ponownym uruchomieniem efektu, bo zmieniła się zależność, oraz gdy komponent jest odmontowywany. Zawsze porządkuje poprzedni efekt, zanim uruchomi następny. Połącz, rozłącz, połącz, rozłącz. Nigdy dwa otwarte połączenia naraz.
Wszystko, co efekt uruchamia i co wymaga wyraźnego zatrzymania, powinno trafić za funkcję cleanup: subscribe wymaga unsubscribe, addEventListener wymaga removeEventListener, setInterval wymaga clearInterval. Ten sam wzorzec naprawia subtelniejszy błąd przy pobieraniu danych, gdzie nieaktualna odpowiedź przychodzi po nowszej:
| |
Jeśli personId zmieni się przed rozwiązaniem pierwszego zapytania, cleanup ustawia ignore na true dla tego zapytania, więc jego callback .then() nie robi nic, zamiast nadpisywać nowszy stan starszymi danymi.
Czemu useEffect wykonuje się dwa razy w trybie deweloperskim
W trybie deweloperskim, pod StrictMode, React wykonuje setup, potem cleanup, potem znowu setup, bezpośrednio po pierwszym zamontowaniu. W produkcji setup wykonuje się raz. React robi to celowo: twój komponent zostanie odmontowany i zamontowany ponownie w realnym użyciu (na przykład wyjście z trasy i powrót do niej), a Strict Mode wymusza, by to zdarzyło się najpierw na twojej maszynie, gdzie brakujący cleanup widać jako widoczny błąd.
Błędem jest łatanie symptomu:
| |
Ten guard blokuje drugie wywołanie connect(), ale wciąż nie ma cleanup, więc połączenie zostaje otwarte po odmontowaniu komponentu. Jeśli ponowne zamontowanie psuje efekt, to efektowi brakuje cleanup. Napisz funkcję cleanup, zamiast bronić się przed drugim wykonaniem.
Częste błędy z zależnościami w useEffect
Obiekt lub funkcja literalna tworzone na nowo przy każdym renderze. Dwa obiekty z identyczną zawartością nie są tym samym obiektem:
| |
Umieść taki obiekt w tablicy zależności, a efekt będzie widział “nową” wartość przy każdym renderze, niezależnie od tego, czy coś istotnego się zmieniło:
| |
Zbuduj obiekt wewnątrz efektu i zależ od prymitywnych wartości, z których jest zrobiony:
| |
Czytanie stanu tylko po to, by policzyć następny stan. Interval, który czyta count, żeby je zwiększyć, zmusza efekt do zależności od count, co odtwarza interval przy każdym ticku:
| |
Aktualizacja funkcyjna usuwa to czytanie, a zależność idzie z nim:
| |
Jeden efekt robiący dwie niezwiązane rzeczy. Załóżmy, że jeden efekt pobiera miasta z kraju i dzielnice z miasta. Z obiema sprawami w tej samej funkcji i tej samej tablicy zależności zmiana miasta ponownie pobiera też miasta. Rozdziel je: jeden efekt na każdy niezależny kawałek synchronizacji, każdy z własną tablicą.
Kiedy nie potrzebujesz useEffect
Dwa wzorce wyglądają, jakby wymagały efektu, a nie wymagają.
Resetowanie stanu, gdy zmienia się prop. Czyszczenie lokalnego stanu formularza, gdy zmienia się userId, jest wyzwaniem do zapisania jako efekt obserwujący userId. Działa, ale komponent renderuje się raz ze starym stanem, zanim reset dojdzie do skutku. Przekaż zamiast tego zmieniającą się wartość jako key komponentu, a React potraktuje go jak nową instancję, bez żadnego efektu po drodze:
| |
Coś, co dzieje się z powodu kliknięcia, nie renderu. Powiadomienie “dodano do koszyka”, które obserwuje product.isInCart w efekcie, odpala się znowu przy każdym przeładowaniu strony, na której produkt już jest w koszyku, bo efekt nie umie odróżnić “właśnie dodane” od “już tam było”. Powiadomienie należy do handlera kliknięcia, który dodaje produkt, gdzie wykonuje się tylko, gdy użytkownik kliknie:
| |
Jeśli coś ma się stać bo użytkownik coś zrobił, umieść to w handlerze zdarzenia tej rzeczy. useEffect jest do tego, co musi zostać zsynchronizowane tak długo, jak komponent jest na ekranie, niezależnie od tego, co robi użytkownik. Synchronizowanie fetcha z cyklem życia komponentu jest tego rodzaju problemem (strona zapytania jest opisana w obsłudze błędów Fetch API), tak jak odłączanie observera przy odmontowaniu, jak w przypadku Intersection Observer API.
Następny raz, gdy efekt się źle zachowuje, zostaw tablicę na początku w spokoju. Wypisz, co czyta ciało funkcji, zdecyduj, czy każde czytanie musi być reaktywne, i sprawdź, czy kod działa bez żadnego efektu. Popraw ciało, a właściwa tablica wyniknie z tego. Jeśli to kolejność asynchronicznych callbacków wokół renderu cię myli, przeczytaj jak działają async/await i event loop, gdzie opisany jest call stack i kolejka microtasków.