Odświeżasz stronę, a ekran pozostaje pusty. Albo może kliknięcie przycisku sprawia, że wentylator procesora wchodzi na najwyższe obroty. Wtedy konsola wyrzuca przerażające ostrzeżenie: Maximum update depth exceeded. React gwałtownie hamuje, ponieważ Twój komponent utknął w nieskończonej pętli. Jest to jeden z najczęstszych błędów w aplikacjach React, i zazwyczaj wynika z prostego nieporozumienia dotyczącego tego, kiedy Twój kod faktycznie się wykonuje.
Aby to naprawić, musisz zrozumieć dokładny moment, w którym render staje się re-renderem, oraz dlaczego zmiany stanu muszą znajdować się poza ścieżką renderowania.
Jak React renderuje Twój komponent
React buduje interfejsy użytkownika z komponentów. W nowoczesnym React te komponenty są funkcjami. Za każdym razem, gdy React musi wyświetlić Twój komponent na ekranie, po prostu wywołuje tę funkcję. Wewnątrz funkcji możesz używać state, aby zapamiętywać rzeczy między renderami. Stan informuje React, które dane należą do komponentu i – co kluczowe – kiedy coś uległo zmianie i interfejs użytkownika (UI) wymaga aktualizacji.
Gdy stan ulega zmianie, React planuje nowy render. Funkcja komponentu uruchamia się ponownie, zwraca nowy JSX, a React aktualizuje DOM, aby był z nim zgodny. W normalnym użytkowaniu ten cykl jest nieszkodliwy. Klikasz przycisk, handler zdarzenia aktualizuje stan, React wykonuje jeden re-render i użytkownik widzi nowy tekst lub kolor.
Błąd pojawia się, gdy sam render wywołuje kolejną aktualizację stanu. Ta nowa aktualizacja stanu wywołuje kolejny render, który wywołuje kolejną aktualizację stanu. React toleruje to przez kilkanaście cykli, a następnie wyrzuca błąd maksymalnej głębokości, aby chronić przeglądarkę przed całkowitym zawieszeniem.
Szybki rzut oka na stan
Zanim przeanalizujemy pętlę, przypomnij sobie, jak działa hook useState. Daje on dokładnie dwie rzeczy: zmienną przechowującą aktualną wartość oraz funkcję do zmiany tej wartości.
const MessageComponent = () => {
const [message, setMessage] = useState('Welcome');
return <h1>{message}</h1>;
};
Tutaj message ma wartość 'Welcome' podczas pierwszego renderu. Jeśli później wywołasz setMessage('Goodbye'), React odnotuje zmianę, ponownie wywoła MessageComponent, a UI wyświetli "Goodbye". Wszystko jest w porządku, ponieważ nic w samym ciele komponentu nie wywołuje funkcji ustawiającej stan automatycznie. Pętla zaczyna się, gdy setter zostaje uruchomiony podczas fazy renderowania bez zewnętrznego zdarzenia.
Wywoływanie setState bezpośrednio w ciele funkcji
Najprostszym sposobem na stworzenie nieskończonej pętli jest wywołanie funkcji ustawiającej stan bezpośrednio w ciele komponentu. Ponieważ ciało komponentu wykonuje się przy każdym renderze, setter uruchamia się przy każdym renderze. Ta nowa aktualizacja stanu powoduje kolejny render. Cykl kręci się w nieskończoność.
Oto jak wygląda ten błąd:
const Counter = () => {
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>;
};
Za każdym razem, gdy Counter się renderuje, zwiększa wartość count. React renderuje ponownie, aby pokazać nową liczbę, widzi ponownie setCount(count + 1) i zwiększa ją jeszcze raz. Rozwiązanie jest proste: nigdy nie wywołuj funkcji ustawiającej stan na najwyższym poziomie komponentu podczas renderowania. Aktualizacje stanu powinny reagować na zdarzenia użytkownika lub efekty uboczne, a nie na sam akt rysowania ekranu. Przenieś tę aktualizację do handlera zdarzenia:
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
Jedynym wyjątkiem od wywoływania setterów podczas renderowania jest sytuacja, gdy obliczasz nowy stan na podstawie propsów, ale nawet wtedy powinieneś użyć innego wzorca, takiego jak bezpośrednie wyprowadzenie wartości lub celowe użycie useEffect.
Przekazywanie wywołania funkcji zamiast referencji
Inną częstą przyczyną jest subtelny błąd w JSX. Podczas przypisywania handlera zdarzenia musisz przekazać samą funkcję. Jeśli przez pomyłkę wywołasz funkcję bezpośrednio w JSX, zostanie ona uruchomiona natychmiast podczas cyklu renderowania.
const Toggle = () => {
const [isOn, setIsOn] = useState(false);
const handleToggle = () => setIsOn(!isOn);
return <button onClick={handleToggle()}>Toggle</button>;
};
Pisząc handleToggle() z nawiasami, nie przekazujesz Reactowi funkcji do wywołania później, gdy użytkownik kliknie. Wywołujesz ją w tej chwili, podczas gdy React buduje wirtualny DOM. Ponieważ handleToggle aktualizuje stan, komponent wykonuje re-render. Podczas tego re-renderu React ponownie widzi handleToggle() i wywołuje ją ponownie. Pętla nigdy się nie kończy. Poprawna wersja usuwa nawiasy:
return <button onClick={handleToggle}>Toggle</button>;
Jeśli musisz przekazać argumenty, owiń wywołanie w funkcję anonimową:
return <button onClick={() => handleToggle(true)}>Switch On</button>;
To rozróżnienie myli nawet doświadczonych programistów podczas refaktoryzacji. Zwracaj uwagę na te nawiasy.
Pułapka zależności w useEffect
Efekty (effects) to właściwe miejsce na efekty uboczne (side effects), takie jak pobieranie danych, synchronizacja z API przeglądarki czy ręczna manipulacja DOM. Jednak useEffect uruchamia się po tym, jak React zatwierdzi render na ekranie. Jeśli Twój efekt aktualizuje stan, React wykona re-render. Zazwyczaj jest to w porządku. Staje się to pętlą, gdy efekt uruchamia się po każdym renderze i zawsze aktualizuje ten sam stan.
Rozważ ten błędny wzorzec:
const UserProfile = () => {
const [user, setUser] = useState({});
useEffect(() => {
setUser({ name: 'Ada', role: 'Admin' });
});
return <div>{user.name}</div>;
};
Ponieważ brakuje tablicy zależności, efekt ten uruchamia się po każdym renderowaniu. Ustawia on user, co wywołuje ponowne renderowanie. Po tym renderowaniu efekt uruchamia się ponownie i znów ustawia user. React wykrywa tę spiralę i wyrzuca błąd.
Rozwiązaniem jest poinformowanie Reacta, kiedy efekt faktycznie powinien się uruchomić, poprzez przekazanie odpowiedniej tablicy zależności. Jeśli efekt ma uruchomić się tylko raz podczas montowania, przekaż pustą tablicę:
useEffect(() => {
setUser({ name: 'Ada', role: 'Admin' });
}, []);
Jeśli efekt zależy od propa lub fragmentu stanu, umieść w tablicy tylko tę zmienną. Należy jednak zachować ostrożność. Umieszczenie zmiennej, która zmienia się przy każdym renderowaniu, spowoduje jedynie odtworzenie tej samej pętli w inny sposób. Na przykład, jeśli umieścisz dosłowny obiekt (object literal) w zależnościach, a obiekt ten jest tworzony na nowo przy każdym renderowaniu komponentu nadrzędnego, efekt będzie uruchamiany w nieskończoność. W takich przypadkach może być konieczne przeniesienie tworzenia obiektu poza komponent lub jego zmemoizowanie.
Praktyczne kroki debugowania
Gdy napotkasz ten błąd, ślad stosu (stack trace) może wydawać się przytłaczający, ponieważ React powtórzył już ten cykl dziesiątki razy. Zacznij od przeczytania początku śladu, aby znaleźć komponent, którego nazwa powtarza się wielokrotnie. Następnie poszukaj setterów stanu w tych trzech miejscach:
- Główna treść komponentu, poza jakimkolwiek handlerem lub hookiem.
- Atrybuty zdarzeń JSX, w których mogłeś napisać
handler()zamiasthandler. - Hooki
useEffect, którym brakuje tablicy zależności lub które zależą od niestabilnych referencji.
Tymczasowo zakomentuj każdy setter stanu, aż błąd przestanie się pojawiać. To powie Ci dokładnie, która aktualizacja jest winowajcą. Jeśli setter znajduje się wewnątrz efektu, zadaj sobie pytanie, czy w ogóle potrzebujesz tam stanu. Czasami programiści ustawiają lokalny stan na podstawie propów wewnątrz efektu, podczas gdy mogliby po prostu użyć propa bezpośrednio w JSX.
Kluczowy wniosek
Błąd „Maximum update depth exceeded” nie jest tajemniczym błędem Reacta. To siatka bezpieczeństwa. Oznacza on, że Twój komponent próbuje renderować się ponownie zamiast czekać na zewnętrzny sygnał. Pozbądź się nawyku traktowania renderowania jako zdarzeń, które powinny generować więcej stanu. Traktuj renderowanie jako czysty skutek stanu, a nie jego przyczynę. Trzymaj aktualizacje stanu wewnątrz handlerów zdarzeń, callbacków lub efektów z starannie dobranymi zależnościami, a nigdy więcej nie zobaczysz tego błędu.
