Menu

Hook useEffect w React: zależności, czyszczenie, przykłady

useEffect uruchamia kod po tym, jak React zaktualizował ekran, żeby komponent mógł synchronizować się z czymś spoza Reacta: timerem, subskrypcją, nasłuchiwaniem zdarzeń. Poznaj trzy kształty tablicy zależności, funkcję czyszczącą, nieskończone pętle i sytuacje, w których efekt w ogóle nie jest potrzebny.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

useEffect uruchamia fragment kodu po tym, jak React wyrenderował twój komponent i zaktualizował ekran. Używasz go, żeby utrzymać komponent w zgodzie z czymś spoza Reacta, jak timer, połączenie sieciowe albo nasłuchiwanie zdarzeń, a to, kiedy uruchamia się ponownie, kontrolujesz tablicą zależności.

Otwórz konsolę pod podglądem: efekt wypisuje log raz, gdy komponent się pojawia, a potem znowu po każdym kliknięciu. Zmień [count] na [] i kliknij ponownie: po pierwszym logu efekt milczy.

Składnia

useEffect(() => {
    // setup: runs after the render is on the screen
    return () => {
        // cleanup: runs before the next setup, and on unmount
    };
}, [dependencies]);
  • Pierwszy argument to funkcja setup. Może zwrócić funkcję czyszczącą (cleanup) albo nic.
  • Drugi argument to tablica zależności: każda wartość z komponentu, którą czyta setup (propsy, stan i wszystko, co z nich wyliczono).
  • Wywołuj useEffect na najwyższym poziomie komponentu, jak każdy hook.

Efekty uruchamiają się po renderowaniu

Renderowanie to wywołanie przez React funkcji twojego komponentu, żeby dowiedzieć się, co ma pokazać ekran. Efekty czekają, aż ta praca się skończy i DOM zostanie zaktualizowany, więc wewnątrz efektu strona pokazuje już nowe wartości.

Każde kliknięcie wypisuje najpierw renderowanie, a potem efekt, a efekt odczytuje tekst przycisku, który React właśnie zapisał. Dlatego odczyt DOM, uruchomienie timera albo połączenie z serwerem należą do efektu, a nie do ciała komponentu: ciało wykonuje się podczas renderowania, zanim cokolwiek jest na ekranie, a React może je wywoływać częściej, niż się spodziewasz.

Efekty zwykle uruchamiają się po tym, jak przeglądarka odmalowała ekran. Gdy efekt musi zmierzyć DOM przed odmalowaniem, użyj zamiast tego useLayoutEffect, który zawsze uruchamia się przed odmalowaniem.

Tablica zależności: trzy kształty

Tablica zależności decyduje, kiedy efekt uruchamia się ponownie. Są trzy kształty, a ten blok używa wszystkich, żebyś mógł zobaczyć różnicę.

Przy ładowaniu wszystkie trzy efekty wypisują log. Kliknij a + 1, a uruchomią się dwa: ten bez tablicy i ten, który zależy od a. Kliknij b + 1, a uruchomi się tylko ten bez tablicy, bo nic w [] ani [a] się nie zmieniło.

Tablica zależnościKiedy uruchamia się efekt
brakPo każdym renderowaniu
[]Raz, po pierwszym renderowaniu (przy montowaniu)
[a, b]Po pierwszym renderowaniu, a potem za każdym razem, gdy zmieni się a albo b

React porównuje każdą zależność z jej poprzednią wartością przez Object.is. Liczby, teksty i wartości logiczne porównuje się po wartości. Obiekty, tablice i funkcje porównuje się po tożsamości, co ma duże znaczenie (zobacz niżej).

Nie pomijaj wartości w tablicy po to, żeby efekt uruchamiał się rzadziej. Efekt dalej używałby wartości ze starego renderowania. Wychwytuje to reguła lintera react-hooks/exhaustive-deps; jest częścią konfiguracji ESLint, którą zawiera szablon React w Vite i konfiguracja ESLint w Next.js.

Czyszczenie

Wiele efektów uruchamia coś, co trzeba zatrzymać: połączenie, subskrypcję, timer. Zwróć funkcję z efektu, a React wywoła ją w dwóch momentach: tuż przed ponownym uruchomieniem efektu z nowymi zależnościami i gdy komponent znika z ekranu (odmontowanie).

Wybierz inny pokój: konsola pokazuje disconnect from general, a potem connect to music. Stare połączenie zawsze zamyka się przed otwarciem nowego. Kliknij Hide, a funkcja czyszcząca uruchomi się ostatni raz, bo ChatRoom został odmontowany.

Funkcja connect zastępuje tu prawdziwego klienta czatu albo WebSocket. Kształt jest taki sam dla każdego z nich: utwórz go w setup, zamknij w funkcji czyszczącej.

Czyszczenie timera

Timer uruchomiony w efekcie działa dalej po zniknięciu komponentu, chyba że funkcja czyszcząca go zatrzyma.

Usuń linię clearInterval(id);, a potem ukryj timer: tick dalej przychodzi do konsoli z komponentu, który już nie istnieje. Każde Show uruchamia kolejny.

Zwróć uwagę na setSeconds((s) => s + 1). Efekt uruchamia się raz, więc zwykłe setSeconds(seconds + 1) na zawsze czytałoby seconds z pierwszego renderowania i zatrzymałoby się na 1. Funkcja aktualizująca zawsze dostaje najnowszą wartość.

Usuwanie nasłuchiwania zdarzeń

Nasłuchiwanie zdarzeń na window albo document stosuje ten sam wzorzec. Przekaż do removeEventListener tę samą funkcję, którą przekazałeś do addEventListener:

useEffect(() => {
    function handleKeyDown(e) {
        if (e.key === 'Escape') onClose();
    }
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose]);

Nieskończona pętla

Efekt, który ustawia wartość stanu, od której też zależy, uruchamia pętlę: stan się zmienia, React renderuje, zależność się zmieniła, efekt się uruchamia i znowu ustawia stan.

// Never stops: every render runs the effect, every effect causes a render
useEffect(() => {
    setCount(count + 1);
});

// Same loop: count changes, so the effect runs again
useEffect(() => {
    setCount(count + 1);
}, [count]);

Komponent renderuje się bez końca, a karta pozostaje zajęta. React nie zatrzymuje tej pętli: w trybie deweloperskim wypisuje w konsoli błąd "Maximum update depth exceeded", a w produkcji nie wypisuje nic. Ten blok uruchamia drugą wersję z warunkiem zatrzymania, żebyś mógł obejrzeć pętlę bez zamrażania strony:

Konsola wypisuje od render 0 do render 5: sześć renderowań przy jednym montowaniu, każde uruchomione przez efekt. Rozwiązania zależą od tego, o co ci chodziło:

  • Chciałeś raz. Użyj [] i formy z funkcją aktualizującą, setCount((c) => c + 1), żeby count nie był zależnością.
  • Wyliczasz wartość z innego stanu. W ogóle jej nie przechowuj; wyliczaj ją podczas renderowania, jak pokazuje ostatnia sekcja tej strony.
  • Reagujesz na akcję użytkownika. Umieść kod w handlerze zdarzenia.

Obiekty i funkcje jako zależności

Obiekt, tablica albo funkcja utworzone w ciele komponentu są nową wartością przy każdym renderowaniu, więc efekt, który od nich zależy, uruchamia się po każdym renderowaniu, nawet gdy nic w środku się nie zmieniło.

Przełączanie kolorów nie ma nic wspólnego z wyszukiwaniem, a jednak każde przełączenie znowu wypisuje search for react. Przenieś linię options do wnętrza efektu i zmień tablicę na [query]: przełączenia ucichną, a wyszukiwanie uruchomi tylko pisanie w polu.

To ogólne rozwiązanie. Twórz obiekty i funkcje wewnątrz efektu, który ich używa, i uzależniaj go od wartości prymitywnych, z których są zbudowane. Gdy funkcja musi żyć poza efektem, useCallback utrzymuje jej tożsamość stabilną między renderowaniami.

Dlaczego efekty uruchamiają się dwa razy w trybie deweloperskim

Przy włączonym <StrictMode> (nowa aplikacja Vite owija w niego root, a App Router w Next.js włącza go domyślnie) React w trybie deweloperskim wykonuje test: montuje każdy komponent, uruchamia jego efekty, uruchamia ich funkcje czyszczące i znowu uruchamia efekty. Dla pokoju czatu powyżej konsola deweloperska pokazuje:

connect to general
disconnect from general
connect to general

Dzieje się to raz, przy montowaniu, i tylko w trybie deweloperskim. Istnieje po to, żeby ujawnić efekty bez funkcji czyszczącej: jeśli dwa uruchomienia z rzędu coś psują, użytkownik zepsułby to samo, wychodząc i wracając. Rozwiązaniem nigdy nie jest blokowanie drugiego uruchomienia refem; jest nim napisanie funkcji czyszczącej. Podgląd na tej stronie działa jak build produkcyjny, więc nie zobaczysz tu podwójnego uruchomienia.

Być może nie potrzebujesz efektu

Efekty służą do synchronizacji z systemami spoza Reacta. Dwa częste zastosowania lepiej działają bez nich.

Wartości wyliczane z propsów albo stanu. Wyliczaj je podczas renderowania. Przechowywanie ich w stanie i synchronizowanie efektem kosztuje dodatkowe renderowanie, a pierwsze z tych renderowań wstawia do DOM nieaktualną wartość:

Każde kliknięcie renderuje WithEffect dwa razy, a jego pierwsze renderowanie wciąż pokazuje stare imię. Computed renderuje się raz z właściwą wartością.

Kod, który odpowiada na akcję użytkownika. Wysłanie formularza, pokazanie powiadomienia po zakupie albo wysłanie zdarzenia analitycznego po kliknięciu należy do handlera zdarzenia. Tam dokładnie wiesz, co się stało, a efekt wie tylko, że zmieniła się jakaś wartość.

useEffectEvent

Czasem efekt potrzebuje najnowszej wartości propsa, ale bez ponownego uruchamiania, gdy ten prop się zmienia. W pokoju czatu powiadomienie powinno używać bieżącego theme, ale zmiana motywu nie powinna łączyć ponownie. React 19.2 dodaje do tego useEffectEvent:

import { useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId, theme }) {
    const onConnected = useEffectEvent(() => {
        showNotification('Connected!', theme); // always the latest theme
    });

    useEffect(() => {
        const connection = createConnection(roomId);
        connection.on('connected', () => onConnected());
        connection.connect();
        return () => connection.disconnect();
    }, [roomId]); // theme is not a dependency
}

Funkcja z useEffectEvent służy tylko do wywoływania wewnątrz efektów. Nie przekazuj jej do innych komponentów ani nie umieszczaj w tablicy zależności.

Najczęściej zadawane pytania

Kiedy uruchamia się useEffect?

Po tym, jak React wyrenderował komponent i zaktualizował DOM, zwykle po tym, jak przeglądarka odmalowała ekran. Bez tablicy zależności uruchamia się po każdym renderowaniu, z [] tylko po pierwszym, a z [a, b] po pierwszym renderowaniu i po każdym, w którym zmieniło się a albo b.

Co to jest funkcja czyszcząca useEffect?

Funkcja, którą zwracasz z efektu. React wywołuje ją przed ponownym uruchomieniem efektu z nowymi zależnościami i jeszcze raz, gdy komponent znika z ekranu. Używaj jej do czyszczenia timerów, usuwania nasłuchiwania zdarzeń i zamykania subskrypcji.

Dlaczego mój useEffect uruchamia się dwa razy?

W trybie deweloperskim <StrictMode> montuje każdy komponent, uruchamia jego efekty, czyści je i uruchamia ponownie, żeby pokazać ci efekty bez funkcji czyszczącej. Dzieje się to raz przy montowaniu i tylko w trybie deweloperskim. Buildy produkcyjne uruchamiają efekt raz.

Dlaczego mój useEffect powoduje nieskończoną pętlę?

Efekt ustawia wartość stanu, która jest też w jego tablicy zależności (albo w ogóle nie ma tablicy). Ustawienie stanu renderuje ponownie, zależność się zmieniła, więc efekt uruchamia się znowu. Użyj funkcji aktualizującej i usuń zależność, dodaj warunek albo wyliczaj wartość podczas renderowania.

Czy callback useEffect może być async?

Nie. Callback musi nie zwracać niczego albo zwracać funkcję czyszczącą, a funkcja async zwraca obietnicę. Zdefiniuj funkcję async wewnątrz efektu i ją wywołaj, jak pokazuje strona o pobieraniu danych.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ