Audyt funkcji useEffect: Te powszechne błędy, ich objawy i rzeczywiste rozwiązania
Rozpoznaj dziesięć najczęstszych błędów useEffect w React – od braku funkcji czyszczenia i przestarzałych zamykających funkcji po warunki wyścigowe i synchronizację propów – oraz zastosuj odpowiednie rozwiązanie.
Źródła: 10 React useEffect Mistakes That Are Secretly Breaking Your App in 2026
React 19, React Compiler oraz Server Components przeniosły wiele zadań z useEffect, ale ten hook nadal jest źródłem wielu najtrudniejszych błędów w React: wycieków danych, przestarzałych informacji, migotania oraz pętli renderowania. Większość z nich wynika z dziesięciu powtarzających się błędów. Poniżej dla każdego z nich podano uszkodzoną wersję, objawy oraz rozwiązanie, a także listę kontrolną do sprawdzenia własnych efektów.
Zasada stojąca za każdym rozwiązaniem: useEffect służy do synchronizacji komponentu z czymś poza React, takim jak timer, socket lub API przeglądarki.
1. Zapomnienie o funkcji czyszczenia
Timery, subskrypcje i słuchacze zdarzeń utworzone w efekcie nadal działają po ponownym renderowaniu lub odmontowaniu komponentu, chyba że je usuniesz. W tym przypadku interwał nigdy nie jest anulowany:
// ❌ Broken: No cleanup
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1);
}, 1000);
// Missing: return () => clearInterval(timer);
}, []);
Objawami są wyciekające interwały oraz powtarzający się listenerzy. Podczas rozwoju tryb Strict Mode celowo montuje, demontuje i ponownie montuje komponenty, aby to ujawnić: bez odpowiedniego oczyszczenia powstają dwa interwały, a licznik zwiększa się dwa razy na sekundę. W środowisku produkcyjnym wyciek pojawia się za każdym razem, gdy komponent jest ponownie montowany. Należy zwrócić funkcję, która cofnie te zmiany:
// ✅ Fixed: Always clean up
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
2. Używanie pustego array zależności w celu oznaczenia „wykonać raz”
Pusty array w żaden szczególny sposób nie oznacza „przy montowaniu”. Informuje on React, że efekt nie zależy od niczego, więc nigdy nie jest ponownie wykonywany. Jeśli efekt odczytuje propsy lub stan, te wartości pozostają nieruchome na poziomie swoich pierwszych wartości renderowania:
// ❌ Broken: Stale closure
function ChatRoom({ roomId }) {
useEffect(() => {
connectToRoom(roomId);
return () => disconnectFromRoom(roomId);
}, []); // roomId changes, but effect never re-runs
}
Gdy roomId ulega zmianie, komponent pozostaje w oryginalnym pokoju. Wymień wszystkie wartości reaktywne, których używa efekt; reguła lintingu react-hooks/exhaustive-deps wykrywa to.
// ✅ Fixed: Include real dependencies
useEffect(() => {
connectToRoom(roomId);
return () => disconnectFromRoom(roomId);
}, [roomId]);
Teraz React rozłącza się z poprzednim pokojem i łączy z nowym za każdym razem, gdy roomId ulega zmianie.
3. Zależności od obiektów tworzonych na nowo przy każdym renderowaniu
Zależności są porównywane pod kątem referencji. Obiekt lub literał tablicy utworzony podczas renderowania stanowi nową referencję za każdym razem, więc efekt, który od niego zależy, jest uruchamiany ponownie po każdym renderowaniu:
// ❌ Broken: New object every render
function Parent() {
const config = { timeout: 5000 }; // New reference every time
useEffect(() => {
fetchData(config);
}, [config]); // Runs every single render
}
Ustabilizuj referencję. Jeśli wartość nigdy się nie zmienia, zdefiniuj ją raz poza komponentem. Jeśli jest tworzona na podstawie propów lub stanu, otocz ją funkcją useMemo z odpowiednimi zależnościami. Poniższy fragment pokazuje obie opcje obok siebie; w praktyce należy wybrać jedną z nich:
// ✅ Fixed: Stable reference
const CONFIG = { timeout: 5000 }; // Defined outside component
function Parent() {
const config = useMemo(() => ({ timeout: 5000 }), []);
useEffect(() => {
fetchData(config);
}, [config]);
}
Często jeszcze prostsze rozwiązanie: polegaj na prymitywach (timeout zamiast config) i utwórz obiekt wewnątrz efektu. Kompilator React memorizuje wiele wartości w kodzie, który kompiluje, ale nie obiekty pochodzące z nekompilowanego kodu lub bibliotek, więc nie polegaj na nim co do poprawności efektów.
4. Pozwalanie na wzajemne wyprzedzanie się asynchronicznych odpowiedzi
Gdy zależność zmienia się szybko, np. przy polu wyszukiwania lub szybkim przewijaniu kart, kilka żądań może być wysyłanych jednocześnie i niekoniecznie muszą zostać zrealizowane w kolejności:
// ❌ Broken: Race condition
useEffect(() => {
fetch(`/api/search?q=${query}`).then(res => res.json()).then(setResults);
}, [query]);
Jeśli użytkownik wpisze „react”, a następnie „react hooks”, odpowiedź dotycząca „react” może nadejść jako ostatnia i przepisać poprawne wyniki. Rozwiązaniem jest anulowanie przestarzałej prośby w funkcji czyszczenia. AbortController anuluje żądanie, a powstały AbortError jest ignorowany, dzięki czemu nie jest raportowany jako prawdziwy błąd:
// ✅ Fixed: Abort in-flight requests
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(res => res.json())
.then(setResults)
.catch(err => {
if (err.name !== 'AbortError') setError(err);
});
return () => controller.abort();
}, [query]);
W przypadku interfejsów wyszukiwania istnieją dodatkowe niuansy, omówione w rozwiązywaniu warunków wyścigowych, których nie da się rozwiązać za pomocą opóźniania.
5. Przechowywanie wartości pochodnych w stanie
Jeśli wartość może zostać obliczona na podstawie istniejących właściwości lub stanu, nie wymaga ona własnego stanu ani efektu:
// ❌ Broken: Redundant state + effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
Każda zmiana jest renderowana dwa razy – najpierw z przestarzałą wartością fullName – co powoduje istnienie dwóch źródeł prawdy. Wartość należy obliczać podczas renderowania, używając useMemo wyłącznie wtedy, gdy operacja jest rzeczywiście kosztowna:
// ✅ Fixed: Compute during render
const fullName = `${firstName} ${lastName}`;// Or for expensive computations
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
Należy pamiętać, że przykład pokazuje dwie alternatywy; deklarowanie fullName dwa razy w tym samym zakresie jest błędem składniowym, więc należy zachować tylko jedną. Szerszy model jest omówiony w bezpieczniejszej alternatywie synchronizacji stanu z useEffect.
6. Tworzenie pętli aktualizacji
Efekt, który ustawia stan, zależy również wiecznie od samego harmonogramu swoich działań:
// ❌ Broken: Infinite loop
useEffect(() => {
setCount(count + 1);
}, [count]);
Aktualizacja wyzwaluje renderowanie, które zmienia wartość count, a zmieniona wartość count ponownie uruchamia efekt, i tak dalej. React ostatecznie zatrzymuje ten proces dzięki błędowi „Maximum update depth exceeded”. Funkcjonalna aktualizacja eliminuje konieczność odczytywania count wewnątrz efektu, więc nie musi on już być elementem zależności:
// ✅ Fixed: Functional update + correct dependencies
useEffect(() => {
const timer = setTimeout(() => {
setCount(c => c + 1); // Functional update avoids dependency
}, 1000);
return () => clearTimeout(timer);
}, []); // No count dependency needed
Należy pamiętać, że to zmienia zachowanie: licznik teraz zwiększa się o jedno tylko sekundę po uruchomieniu komponentu. Aby uzyskać licznik, który odlicza w czasie rzeczywistym, należy użyć setInterval wraz z funkcją czyszczenia, jak w przykładzie błędnym nr 1. Każde wywołanie setState w efekcie musi zawierać warunek gwarantujący jego zakończenie.
7. Całkowite pominięcie tablicy zależności
Bez drugiego argumentu efekt jest uruchamiany po każdym renderowaniu. Czasami tak właśnie chcemy, ale zazwyczaj jest to błąd i generuje duże obciążenie:
// ❌ Broken: Runs on every render
useEffect(() => {
console.log('Effect running');
performHeavyComputation(data);
});
Kompilator React ogranicza niepotrzebne ponowne renderowanie, ale nie sprawia, że operacje wewnątrz efektów są szybsze, więc efekt wykonywający intensywne obliczenia bez ochrony pozostaje wąskim gardłem. Określ rzeczywistą zależność:
// ✅ Fixed: Explicit dependencies
useEffect(() => {
performHeavyComputation(data);
}, [data]);
8. Obsługa zdarzeń użytkownika w efektach
Efekty są uruchamiane, ponieważ coś zostało wyrenderowane, a nie dlatego, że użytkownik coś zrobił. Analizy powiązane z flagą stanu są uruchamiane za każdym razem, gdy ta flaga ulega zmianie, bez względu na przyczynę:
// ❌ Broken: Event logic in effect
useEffect(() => {
if (isModalOpen) {
trackEvent('modal_opened');
}
}, [isModalOpen]);
To zdarzenie jest rejestrowane podczas inicializacji, jeśli modala jest już otwarta, gdy rodzic zmienia właściwość lub gdy stan zostaje przywrócony, a nie tylko wtedy, gdy użytkownik kliknie. Umieść logikę interakcji w obsłudze, która ją realizuje:
// ✅ Fixed: Event handler
function handleOpenModal() {
setIsModalOpen(true);
trackEvent('modal_opened');
}
Jeśli możesz nazwać działanie użytkownika, które powinno uruchomić kod, należy ono do obsługi.
9. Pobieranie danych za pomocą prostego efektu
Dzięki komponentom serwerowym oraz narzędziom do ładowania frameworków, które zajmują się początkowymi danymi w wielu aplikacjach, pobieranie danych po stronie klienta za pomocą efektów jest potrzebne rzadziej. Gdy mimo to dokonuje się pobierania danych po stronie klienta, surowy efekt jest kruchy:
// ❌ Broken: Raw fetch in effect
useEffect(() => {
fetch('/api/user').then(r => r.json()).then(setUser);
}, []);
Brakuje mu stanu ładowania, obsługi błędów, eliminacji duplikatów zapytań, cache’owania, a jak pokazuje błąd nr 4, także ochrony przed konfliktami. Biblioteka do pobierania danych zajmuje się tym wszystkim:
// ✅ Fixed: Use a data-fetching library
const { data: user, isLoading, error } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
staleTime: 5 * 60 * 1000,
});
TanStack Query i SWR to najczęściej wybierane rozwiązania. Jeśli ich nie ma, należy napisać własny hook z obsługą stanu ładowania, błędów i anulowania operacji zamiast używać ad-hoc efektów do pobierania danych.
10. Kopiowanie propów do stanu i synchronizacja ich
Komponent, który otrzymuje wartość początkową, kopiuje ją do stanu, a następnie używa efektu do odzwierciedlania zmian w propach, działa sprzecznie z samym sobą:
// ❌ Broken: Prop-to-state sync
function EmailInput({ initialEmail }) {
const [email, setEmail] = useState(initialEmail);
useEffect(() => {
setEmail(initialEmail); // Sync prop to state
}, [initialEmail]);
return <input value={email} onChange={e => setEmail(e.target.value)} />;
}
Każdy raz, gdy rodzic dostarcza nową wartość initialEmail, wpisywane przez użytkownika dane są cicho nadpisywane po dodatkowym wyświetleniu z dawną wartością. Należy wybrać jedną z opcji: uczynić komponent w pełni kontrolowanym lub pozostawić go niekontrolowanym i celowo sformatować go poprzez zmianę jego key, co daje React nową instancję z aktualnym stanem:
// ✅ Fixed: Controlled or fully uncontrolled
// Option 1: Fully controlled — no internal state
function EmailInput({ email, onChange }) {
return <input value={email} onChange={onChange} />;
}
// Option 2: Fully uncontrolled with key
function EmailInput({ initialEmail }) {
const [email, setEmail] = useState(initialEmail);
return <input value={email} onChange={e => setEmail(e.target.value)} />;
}
// Usage: Reset by changing key
<EmailInput key={userId} initialEmail={user.email} />
Jak React 19 zmienia sytuację
Nowsze wersje React sprawiają, że takie błędy stają się bardziej widoczne, ale jednocześnie mniej konieczne:
- Rozszerzone montowanie efektów w trybie Strict Mode przeznaczonym wyłącznie do rozwoju pozwala wcześnie zidentyfikować brakujące lub błędne operacje czyszczenia.
- Kompilator React automatycznie zapamiętuje komponenty oraz wiele wartości, co zmniejsza przypadkowe ponowne wyświetlanie, ale nie naprawia logiki efektów.
use może odczytywać obietnice i kontekst podczas renderowania, co eliminuje niektóre powody do używania efektów w przypadku wartości asynchronicznych.Lista kontrolna przed połączeniem efektów
Zanim efekt trafi do użycia, sprawdź:
- Każda subskrypcja, timer i słuchacz mają odpowiadające im procedury czyszczenia.
- Tablica zależności zawiera wszystkie wartości reaktywne, które efekt odczytuje.
- Zależności typu obiekt i tablica są stabilne lub zastąpione zależnościami prymitywnymi.
- Zadania asynchroniczne są anulowane lub ignorowane, gdy zmieniają się zależności.
- Nic z tego, co jest obliczane w efekcie, nie może być po prostu wygenerowane podczas renderowania.
- Żadna aktualizacja stanu nie może ponownie uruchomić tego samego efektu bez warunku zatrzymania.
- Logika uruchamiana przez użytkownika znajduje się w obsługiwaczach zdarzeń.
- Pobieranie danych odbywa się za pomocą komponentu serwerowego, ładowacza frameworka lub biblioteki do pobierania danych, jeśli to możliwe.
- Każdy element stanu ma dokładnie jedno źródło prawdy.
Podsumowanie
Prawie każdy błąd w funkcji useEffect wynika z używania tego hooka do czegoś innego niż synchronizacja z zewnętrznymi źródłami. Wartości pochodne powinny znajdować się w funkcji renderowania, działania użytkownika – w obsługiwaczach zdarzeń, dane serwerowe – w warstwie danych, a stan, który można sformatować na nowo – za pomocą key. Prawdziwe subskrypcje, które pozostają, wymagają dokładnych zależności oraz mechanizmów czyszczenia. Często najlepszym rezultatem audytu jest usunięcie niepotrzebnej funkcji useEffect.
Literatura pokrewna
- Sześć błędów w timingu asynchronicznym, które przejawiają się jako błędy w renderowaniu Reacta — Dowiedz się, jak rozpoznawać sześć wzorców JavaScriptu asynchronicznego, od przestarzałych wartości zapisanych w pamięci po brakujące zwroty typu Promise, które powodują, że komponenty React pokazują niewłaściwy lub niemożliwy stan.
- Stwórz przykładowy useState, aby zrozumieć snapshoty, kolejki i effects — Przekonaj się, jak zachowują się hooki React, budując mały przykład useState, a następnie wykorzystaj ten model do wyjaśnienia kolejek aktualizacji, przestarzałych snapshotów, zamykających funkcji, czyszczenia efektów, referencji oraz grupowania operacji.