useEffect
useEffect
to hook reactowy, który pozwala synchronizować komponent z zewnętrznym systemem.
useEffect(setup, dependencies?)
- Dokumentacja
- Sposób użycia
- Łączenie z zewnętrznym systemem
- Opakowywanie efektów we własne hooki
- Sterowanie widżetem niewykorzystującym Reacta
- Pobieranie danych przy użyciu efektów
- Określanie reaktywnych zależności
- Aktualizacja stanu w efekcie na podstawie poprzedniego stanu
- Usuwanie niepotrzebnych zależności od obiektów
- Usuwanie niepotrzebnych zależności od funkcji
- Odczytywanie najnowszych właściwości i stanu z efektu
- Wyświetlanie różnych treści na serwerze i kliencie
- Znane problemy
- Mój efekt jest uruchamiany podwójnie, gdy komponent jest montowany
- Mój efekt uruchamia się po każdym przerenderowaniu
- Mój efekt wpada w nieskończoną pętlę
- Moja funkcja czyszcząca jest uruchamiana nawet wtedy, gdy mój komponent nie jest odmontowywany
- Mój efekt robi coś wizualnego i widzę migotanie przed jego uruchomieniem
Dokumentacja
useEffect(setup, dependencies?)
Aby zadeklarować efekt, wywołaj useEffect
na głównym poziomie swojego komponentu:
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
Zobacz więcej przykładów poniżej
Parametry
-
setup
: funkcja z logiką efektu (inaczej nazywana funkcją konfigurującą). Funkcja ta może również opcjonalnie zwracać funkcję czyszczącą (ang. cleanup function). Gdy komponent zostaje dodany do drzewa DOM, React uruchamia funkcję konfigurującą. Po każdym ponownym renderowaniu, gdy zmienią się zależności, React najpierw uruchamia funkcję czyszczącą (jeśli została zdefiniowana) z poprzednimi wartościami, a następnie funkcję konfigurującą z nowymi wartościami. Gdy komponent zostanie usunięty z drzewa DOM, React uruchamia funkcję czyszczącą. -
opcjonalnie
dependencies
: lista wszystkich reaktywnych wartości użytych w kodzie funkcji konfigurującej. Wartościami reaktywnymi są między innymi właściwości, stany oraz wszystkie zmienne i funkcje zadeklarowane bezpośrednio w ciele twojego komponentu. Jeśli twój linter jest skonfigurowany pod Reacta, będzie on sprawdzał, czy każda wartość reaktywna jest poprawnie dodana do zależności. Lista zależności musi mieć stałą liczbę elementów i być zapisana w miejscu wywołania, jak np.[dep1, dep2, dep3]
. React porównuje każdą zależność ze swoją poprzednią wartością, używając porównaniaObject.is
. Jeśli pominiesz ten parametr, efekt zostanie uruchomiony ponownie po każdym renderowaniu komponentu. Zobacz różnicę między przekazywaniem tablicy zależności, pustej tablicy a brakiem zależności w ogóle.
Zwracana wartość
useEffect
zwraca undefined
.
Zastrzeżenia
-
useEffect
jest hookiem, więc można go wywoływać tylko na głównym poziomie komponentu lub w innych hookach. Nie można go wywoływać wewnątrz pętli czy instrukcji warunkowej. Jeśli tego potrzebujesz, wyodrębnij nowy komponent i przenieś do niego stan. -
Jeśli nie próbujesz synchronizować się z jakimś zewnętrznym systemem, prawdopodobnie nie potrzebujesz efektu.
-
W trybie rygorystycznym (ang. Strict Mode), React w środowisku developerskim wywoła dodatkowo funkcję konfigurującą i funkcję czyszczącą jeszcze przed pierwszym właściwym wywołaniem tej pierwszej. Jest to rodzaj testu obciążeniowego, który pozwala upewnić się, że logika funkcji czyszczącej “odzwierciedla” logikę funkcji konfigurującej i że zatrzymuje lub cofa to, co ona robi. Jeśli to powoduje problemy, zaimplementuj funkcję czyszczącą.
-
Jeśli niektóre z twoich zależności to obiekty lub funkcje zdefiniowane wewnątrz komponentu, istnieje ryzyko, że spowodują, że efekt będzie wykonywał się częściej niż jest to potrzebne. Aby to naprawić, usuń zbędne zależności od obiektów i funkcji. Możesz również wydzielić aktualizacje stanu oraz logikę niereaktywną poza efekt.
-
Jeśli twój efekt nie został wywołany przez interakcję (np. kliknięcie), React zazwyczaj pozwoli przeglądarce najpierw odświeżyć ekran przed uruchomieniem twojego efektu. Jeśli efekt ten wykonuje jakieś operacje związane z wyświetlaniem (np. ustawianie pozycji dymka (ang. tooltip)) i opóźnienie jest zauważalne (np. występuje migotanie), zastąp
useEffect
przezuseLayoutEffect
. -
Nawet jeśli twój efekt został wywołany przez interakcję (np. kliknięcie), przeglądarka może odświeżyć ekran przed przetworzeniem aktualizacji stanu wewnątrz twojego efektu. Zazwyczaj jest to pożądane zachowanie. Niemniej jednak, jeśli chcesz zablokować przeglądarkę przed odświeżaniem ekranu, musisz zastąpić
useEffect
przezuseLayoutEffect
. -
Efekty uruchamiane są tylko po stronie klienta. Nie są uruchamiane podczas renderowania po stronie serwera.
Sposób użycia
Łączenie z zewnętrznym systemem
Niektóre komponenty muszą pozostać połączone z siecią, pewnym interfejsem przeglądarki lub zewnętrzną biblioteką podczas wyświetlania ich na ekranie. Systemy te nie są kontrolowane przez Reacta, dlatego nazywane są zewnętrznymi.
Aby połączyć swój komponent z zewnętrznym systemem, wywołaj funkcję useEffect
na głównym poziomie swojego komponentu:
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
Aby użyć useEffect
, musisz przekazać dwa argumenty:
- Funkcję konfigurującą z kodem konfigurującym, który łączy się z tym systemem.
- Funkcja ta powinna zwracać funkcję czyszczącą z kodem czyszczącym, który rozłącza się z tym systemem.
- Tablicę zależności, zawierającą każdą wartość z twojego komponentu używaną wewnątrz tych funkcji.
React wywołuje twoje funkcje konfigurującą i czyszczącą wtedy, gdy jest to konieczne, co może się zdarzyć wielokrotnie:
- Twój kod konfigurujący jest wykonywany, gdy komponent jest dodawany do strony (montowany).
- Po każdym renderowaniu twojego komponentu, w którym zależności uległy zmianie:
- Najpierw jest wykonywany twój kod czyszczący z poprzednimi właściwościami i stanem.
- Następnie jest wykonywany twój kod konfigurujący z nowymi właściwościami i stanem.
- Twój kod czyszczący jest wykonywany jeszcze raz po usunięciu (odmontowaniu) komponentu ze strony.
Przyjrzyjmy się tej sekwencji dla przykładu powyżej.
Kiedy komponent ChatRoom
zostanie dodany do strony, połączy się z pokojem czatu przy użyciu początkowych serverUrl
i roomId
. Jeśli którakolwiek z zależności - serverUrl
lub roomId
- zmieni się w wyniku przerenderowania (np. jeśli użytkownik wybierze inny pokój czatu z rozwijanej listy), twój efekt rozłączy się z poprzednim pokojem i połączy się z następnym. Kiedy komponent ChatRoom
zostanie usunięty ze strony, twój efekt rozłączy się ostatni raz.
Aby pomóc w wykrywaniu błędów, w trybie developerskim React wykonuje dodatkowo funkcję konfigurującą oraz czyszczącą przed właściwym, docelowym wywołaniem konfigurującej. Jest to test obciążeniowy, który sprawdza, czy logika twojego efektu jest poprawnie zaimplementowana. Jeśli to spowoduje widoczne problemy, oznacza to, że brakuje pewnej logiki w funkcji czyszczącej. Funkcja ta powinna zatrzymać lub cofnąć wszystko, co zrobiła funkcja konfigurująca. Ogólnie rzecz biorąc, użytkownik nie powinien być w stanie rozróżnić między jednorazowym wywołaniem konfigurowania (jak na produkcji), a sekwencją konfigurowanie → czyszczenie → konfigurowanie (jak w trybie developerskim). Zobacz najczęstsze rozwiązania.
Postaraj się pisać każdy efekt jako niezależny proces i skup się na pojedynczym cyklu konfigurowania i czyszczenia w danym momencie. Nie ma znaczenia, czy komponent jest montowany, aktualizowany czy odmontowywany. Jeśli logika czyszczenia poprawnie odwzorowuje logikę konfigurowania, twój efekt jest odporny na uruchamianie konfigurowania i czyszczenia tak często, jak to konieczne.
Przykład 1 z 5: Łączenie się z serwerem czatu
W tym przykładzie komponent ChatRoom
wykorzystuje efekt do utrzymania połączenia z systemem zewnętrznym zdefiniowanym w pliku chat.js
. Naciśnij “Otwórz czat”, aby pojawił się komponent ChatRoom
. Ta piaskownica działa w trybie developerskim, więc ma miejsce dodatkowy cykl łączenia i rozłączania, tak jak jest to wyjaśnione tutaj. Spróbuj zmieniać roomId
i serverUrl
za pomocą rozwijanej listy i pola tekstowego, a zobaczysz, jak efekt ponownie łączy się z czatem. Naciśnij “Zamknij czat”, aby zobaczyć, jak efekt kończy połączenie po raz ostatni.
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => { connection.disconnect(); }; }, [roomId, serverUrl]); return ( <> <label> URL serwera:{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Witaj w pokoju: {roomId}</h1> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); const [show, setShow] = useState(false); return ( <> <label> Wybierz pokój czatu:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">ogólny</option> <option value="travel">podróże</option> <option value="music">muzyka</option> </select> </label> <button onClick={() => setShow(!show)}> {show ? 'Zamknij czat' : 'Otwórz czat'} </button> {show && <hr />} {show && <ChatRoom roomId={roomId} />} </> ); }
Opakowywanie efektów we własne hooki
Efekty są rodzajem “ukrytej furtki”: używamy ich, gdy potrzebujemy “wyjść poza Reacta” i nie ma innego lepszego sposobu w danym przypadku. Jeśli często zdarza ci się ręcznie tworzyć efekty, zwykle oznacza to, że należy wyodrębnić własne hooki, które implementują wspólne zachowania, na których polegają twoje komponenty.
Przykładowo, własny hook useChatRoom
“ukrywa” logikę efektu za bardziej deklaratywnym interfejsem:
function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
Następnie, możesz go użyć w dowolnym komponencie w taki sposób:
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...
W ekosystemie Reacta jest wiele świetnych własnych hooków, które można użyć w różnych przypadkach.
Przykład 1 z 3: Własny hook useChatRoom
Ten przykład jest identyczny jak jeden z wcześniejszych przykładów, ale logika została wyodrębniona do własnego hooka.
import { useState } from 'react'; import { useChatRoom } from './useChatRoom.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); useChatRoom({ roomId: roomId, serverUrl: serverUrl }); return ( <> <label> URL serwera:{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Witaj w pokoju: {roomId}</h1> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); const [show, setShow] = useState(false); return ( <> <label> Wybierz pokój czatu:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">ogólny</option> <option value="travel">podróże</option> <option value="music">muzyka</option> </select> </label> <button onClick={() => setShow(!show)}> {show ? 'Zamknij czat' : 'Otwórz czat'} </button> {show && <hr />} {show && <ChatRoom roomId={roomId} />} </> ); }
Sterowanie widżetem niewykorzystującym Reacta
Czasami chcesz, aby zewnętrzny system był zsynchronizowany z jakąś właściwością lub stanem twojego komponentu.
Na przykład, jeśli masz widżet mapy z zewnętrznej biblioteki lub komponent odtwarzacza wideo napisany bez użycia Reacta, możesz wykorzystać efekt, aby wywołać metody, które dostosowują jego stan do aktualnego stanu twojego reactowego komponentu. Ten efekt tworzy instancję klasy MapWidget
zdefiniowanej w pliku map-widget.js
. Kiedy zmienisz właściwość zoomLevel
komponentu Map
, efekt wywoła metodę setZoom()
na instancji klasy, aby utrzymać ich synchronizację:
import { useRef, useEffect } from 'react'; import { MapWidget } from './map-widget.js'; export default function Map({ zoomLevel }) { const containerRef = useRef(null); const mapRef = useRef(null); useEffect(() => { if (mapRef.current === null) { mapRef.current = new MapWidget(containerRef.current); } const map = mapRef.current; map.setZoom(zoomLevel); }, [zoomLevel]); return ( <div style={{ width: 200, height: 200 }} ref={containerRef} /> ); }
W tym przykładzie nie jest potrzebna funkcja czyszcząca, ponieważ klasa MapWidget
steruje tylko węzłem DOM, który został do niej przekazany. Po usunięciu reactowego komponentu Map
z drzewa, zarówno węzeł DOM, jak i instancja klasy MapWidget
zostaną automatycznie posprzątane przez javascriptowy program czyszczenia pamięci (ang. garbage collector).
Pobieranie danych przy użyciu efektów
Możesz użyć efektu do pobierania danych dla swojego komponentu. Zauważ, że jeśli korzystasz z frameworka, użycie mechanizmu pobierania danych dostarczonego przez niego będzie o wiele wydajniejsze niż ręczne pisanie efektów.
Jeśli chcesz ręcznie pobierać dane za pomocą efektu, twój kod może wyglądać tak:
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';
export default function Page() {
const [person, setPerson] = useState('Alicja');
const [bio, setBio] = useState(null);
useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
};
}, [person]);
// ...
Zwróć uwagę na zmienną ignore
, która jest inicjowana jako false
i ustawiana na true
podczas czyszczenia. Zapewnia to, że twój kod nie będzie podatny na tzw. “wyścigi” (ang. race conditions): odpowiedzi z sieci mogą przychodzić w innej kolejności, niż zostały wysłane żądania.
import { useState, useEffect } from 'react'; import { fetchBio } from './api.js'; export default function Page() { const [person, setPerson] = useState('Alicja'); const [bio, setBio] = useState(null); useEffect(() => { let ignore = false; setBio(null); fetchBio(person).then(result => { if (!ignore) { setBio(result); } }); return () => { ignore = true; } }, [person]); return ( <> <select value={person} onChange={e => { setPerson(e.target.value); }}> <option value="Alicja">Alicja</option> <option value="Barbara">Barbara</option> <option value="Tadeusz">Tadeusz</option> </select> <hr /> <p><i>{bio ?? 'Ładowanie...'}</i></p> </> ); }
Możesz również przepisać ten kod używając składni async
/ await
, ale nadal musisz napisać funkcję czyszczącą:
import { useState, useEffect } from 'react'; import { fetchBio } from './api.js'; export default function Page() { const [person, setPerson] = useState('Alicja'); const [bio, setBio] = useState(null); useEffect(() => { async function startFetching() { setBio(null); const result = await fetchBio(person); if (!ignore) { setBio(result); } } let ignore = false; startFetching(); return () => { ignore = true; } }, [person]); return ( <> <select value={person} onChange={e => { setPerson(e.target.value); }}> <option value="Alicja">Alicja</option> <option value="Barbara">Barbara</option> <option value="Tadeusz">Tadeusz</option> </select> <hr /> <p><i>{bio ?? 'Ładowanie...'}</i></p> </> ); }
Pisanie kodu do pobierania danych bezpośrednio w efektach staje się powtarzalne i sprawia, że później trudniej jest dodać optymalizacje, takie jak buforowanie (ang. cache) lub renderowanie po stronie serwera (ang. server rendering). Łatwiej jest użyć własnych hooków - albo stworzonych przez ciebie, albo utrzymywanych przez społeczność.
Dla dociekliwych
Pisanie wywołań fetch
wewnątrz efektów to popularny sposób na pobieranie danych, zwłaszcza w aplikacjach działających w pełni po stronie klienta. Jest to jednak podejście wymagające dużej ilości ręcznej pracy i ma istotne wady:
- Efekty nie są uruchamiane na serwerze. Oznacza to, że początkowy HTML renderowany po stronie serwera będzie zawierał jedynie stan ładowania bez danych. Komputer klienta musiałby pobrać cały kod JavaScript i wyrenderować aplikację, tylko po to, by odkryć, że teraz musi pobrać dane. To nie jest zbyt wydajne podejście.
- Bezpośrednie pobieranie danych w efektach sprzyja tworzeniu “kaskad żądań sieciowych” (ang. network waterfall). Renderujesz komponent rodzica, on pobiera pewne dane, renderuje komponenty potomne, a następnie one zaczynają pobierać swoje dane. Jeśli sieć nie jest zbyt szybka, takie podejście jest to znacznie wolniejsze niż równoczesne pobieranie wszystkich danych.
- Pobieranie bezpośrednio w efektach zazwyczaj oznacza brak wstępnego wczytywania (ang. preload) i buforowania danych (ang. cache). Na przykład, jeśli komponent jest odmontowywany, a następnie ponownie montowany, będzie trzeba ponownie pobrać dane.
- Nie jest to zbyt ergonomiczne. Pisanie wywołania
fetch
w taki sposób, aby uniknąć błędów takich jak wyścigi, wymaga dość dużej ilości kodu.
Te wady nie dotyczą tylko Reacta. Występują one przy pobieraniu danych podczas montowania komponentu przy użyciu dowolnej biblioteki. Podobnie jak w przypadku routingu, poprawne wykonywanie pobierania danych nie jest proste, dlatego polecamy następujące podejścia:
- Jeśli używasz frameworka, wykorzystaj jego wbudowany mechanizm pobierania danych. Współczesne frameworki reactowe mają zintegrowane mechanizmy pobierania danych, które są wydajne i rozwiązują powyższe problemy.
- W przeciwnym razie, rozważ użycie lub zbudowanie pamięci podręcznej po stronie klienta. Popularne rozwiązania open source obejmują React Query, useSWR oraz React Router 6.4+. Możesz także zbudować swoje własne rozwiązanie, w którym byłyby użyte efekty, ale także zawarte byłyby: logika do unikania zduplikowanych zapytań, buforowania odpowiedzi i unikania kaskad żądań sieciowych (poprzez wstępne wczytywanie danych lub przeniesienie wymagań dot. danych do ścieżek).
Możesz nadal pobierać dane bezpośrednio w efektach, jeśli żadne z wymienionych podejść nie spełnia twoich potrzeb.
Określanie reaktywnych zależności
Zauważ, że nie możesz dowolnie “wybrać” zależności twojego efektu. Każda reaktywna wartość użyta w kodzie twojego efektu musi być zadeklarowana jako zależność. Tablica zależności twojego efektu jest określana przez otaczający kod:
function ChatRoom({ roomId }) { // To jest wartość reaktywna
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // To też jest wartość reaktywna
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Ten efekt odczytuje te wartości reaktywne
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ Musisz zatem określić je jako zależności twojego efektu
// ...
}
Jeśli zmieni się serverUrl
lub roomId
, twój efekt ponownie połączy się z czatem, używając nowych wartości.
Wartościami reaktywnymi są właściwości oraz wszystkie zmienne i funkcje zadeklarowane bezpośrednio wewnątrz twojego komponentu. Ponieważ roomId
i serverUrl
są wartościami reaktywnymi, nie możesz ich pomijać w zależnościach. Jeśli spróbujesz je pominąć i twój linter jest poprawnie skonfigurowany pod Reacta, wskaże on to jako błąd, który musisz poprawić:
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 Reactowy hook useEffect ma brakujące zależności: 'roomId' i 'serverUrl'
// ...
}
Aby usunąć zależność, musisz “udowodnić” linterowi, że to nie musi być zależność. Na przykład, możesz przenieść serverUrl
poza swój komponent i tym samym udowodnić, że nie jest wartością reaktywną i nie zmieni się podczas ponownego renderowania:
const serverUrl = 'https://localhost:1234'; // To nie jest już wartość reaktywna
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Wszystkie zależności zadeklarowane
// ...
}
Teraz, kiedy serverUrl
nie jest już wartością reaktywną (i nie może zmienić się podczas ponownego renderowania), nie musi być zależnością. Jeśli kod twojego efektu nie używa żadnych wartości reaktywnych, jego tablica zależności powinna być pusta ([]
):
const serverUrl = 'https://localhost:1234'; // To nie jest już wartość reaktywna
const roomId = 'music'; // To nie jest już wartość reaktywna
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ Wszystkie zależności zadeklarowane
// ...
}
Efekt z pustymi zależnościami nie zostanie ponownie uruchomiony, gdy zmieni się którakolwiek właściwość lub stan twojego komponentu.
Przykład 1 z 3: Przekazywanie tablicy zależności
Jeśli określisz zależności, twój efekt zostanie uruchomiony po początkowym renderowaniu oraz po ponownym renderowaniu ze zmienionymi zależnościami.
useEffect(() => {
// ...
}, [a, b]); // Zostanie uruchomiony ponownie, jeśli a lub b ulegną zmianie
W poniższym przykładzie serverUrl
i roomId
to wartości reaktywne, więc obie muszą być określone jako zależności. W rezultacie wybór innego pokoju z rozwijanej listy lub edycja adresu URL serwera powoduje ponowne połączenie się czatu. Z kolei message
nie jest używany wewnątrz efektu (przez co nie jest zależnością), więc edycja wiadomości nie spowoduje ponownego połączenia się z czatem.
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); const [message, setMessage] = useState(''); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => { connection.disconnect(); }; }, [serverUrl, roomId]); return ( <> <label> URL serwera:{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Witaj w pokoju: {roomId}</h1> <label> Twoja wiadomość:{' '} <input value={message} onChange={e => setMessage(e.target.value)} /> </label> </> ); } export default function App() { const [show, setShow] = useState(false); const [roomId, setRoomId] = useState('general'); return ( <> <label> Wybierz pokój czatu:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">ogólny</option> <option value="travel">podróże</option> <option value="music">muzyka</option> </select> <button onClick={() => setShow(!show)}> {show ? 'Zamknij czat' : 'Otwórz czat'} </button> </label> {show && <hr />} {show && <ChatRoom roomId={roomId}/>} </> ); }
Aktualizacja stanu w efekcie na podstawie poprzedniego stanu
Gdy w efekcie chcesz zaktualizować stan na podstawie poprzedniego stanu, możesz napotkać na pewien problem:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // Chcesz zwiększać licznik co sekundę...
}, 1000)
return () => clearInterval(intervalId);
}, [count]); // 🚩 ...ale podanie `count` jako zależności zawsze resetuje interwał.
// ...
}
Ponieważ count
jest wartością reaktywną, musi być określona na liście zależności. Jednakże to powoduje uruchamianie się czyszczenia i efektu ponownie za każdym razem, gdy zmieni się count
. Nie jest to idealne rozwiązanie.
Aby to naprawić, przekaż funkcje aktualizującą c => c + 1
do setCount
:
import { useState, useEffect } from 'react'; export default function Counter() { const [count, setCount] = useState(0); useEffect(() => { const intervalId = setInterval(() => { setCount(c => c + 1); // ✅ Przekaż funkcję aktualizującą }, 1000); return () => clearInterval(intervalId); }, []); // ✅ Teraz count nie jest juź zależnością return <h1>{count}</h1>; }
Teraz, kiedy przekazujesz funkcję c => c + 1
zamiast count + 1
, twój efekt mie musi już zależeć od count
. W rezultacie tej poprawki, nie ma już potrzeby zatrzymywania i ponownego uruchamiania interwału za każdym razem, gdy zmienia się count
.
Usuwanie niepotrzebnych zależności od obiektów
Jeśli twój efekt zależy od obiektu lub funkcji utworzonej podczas renderowania, może być uruchamiany zbyt często. Na przykład, ten efekt łączy się ponownie po każdym renderowaniu, ponieważ obiekt options
jest inny w każdym renderowaniu:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = { // 🚩 Ten obiekt jest tworzony od początku przy każdym renderowaniu
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options); // Jest on użyty w efekcie
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 W rezultacie, ta zależność bedzie inna w każdym renderowaniu
// ...
Unikaj używania obiektu utworzonego podczas renderowania jako zależności. Zamiast tego, stwórz obiekt wewnątrz efektu:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); useEffect(() => { const options = { serverUrl: serverUrl, roomId: roomId }; const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [roomId]); return ( <> <h1>Witaj w pokoju: {roomId}</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Wybierz pokój czatu:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">ogólny</option> <option value="travel">podróże</option> <option value="music">muzyka</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
Teraz, kiedy tworzysz obiekt options
wewnątrz efektu, sam efekt zależy już tylko od ciągu znaków roomId
.
Dzięki tej poprawce, pisanie w polu tekstowym nie powoduje ponownego łączenia się z czatem. W przeciwieństwie do obiektu, który jest tworzony na nowo, ciąg znaków taki jak roomId
nie zmienia się, chyba że zostanie ustawiony na inną wartość. Dowiedz się więcej o usuwaniu zależności.
Usuwanie niepotrzebnych zależności od funkcji
Jeśli twój efekt zależy od obiektu lub funkcji utworzonej podczas renderowania, może być uruchamiany zbyt często. Na przykład, ten efekt łączy się ponownie po każdym renderowaniu, ponieważ funkcja createOptions
jest inna w każdym renderowaniu:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() { // 🚩 Ta funkcja jest tworzona od początku przy każdym renderowaniu
return {
serverUrl: serverUrl,
roomId: roomId
};
}
useEffect(() => {
const options = createOptions(); // Jest ona użyta w efekcie
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🚩 W rezultacie, ta zależność bedzie inna w każdym renderowaniu
// ...
Tworzenie funkcji od nowa przy każdym renderowaniu nie stanowi problemu samo w sobie. Nie musisz tego optymalizować. Jednakże, jeśli używasz jej jako zależności w swoim efekcie, spowoduje to ponowne uruchomienie efektu po każdym renderowaniu.
Unikaj używania jako zależności funkcji utworzonej podczas renderowania. Zamiast tego zadeklaruj ją wewnątrz efektu:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); useEffect(() => { function createOptions() { return { serverUrl: serverUrl, roomId: roomId }; } const options = createOptions(); const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [roomId]); return ( <> <h1>Witaj w pokoju: {roomId}</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Wybierz pokój czatu:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">ogólny</option> <option value="travel">podróże</option> <option value="music">muzyka</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
Teraz, kiedy definiujesz funkcję createOptions
wewnątrz efektu, sam efekt zależy już tylko od ciągu znaków roomId
. Dzięki tej poprawce, pisanie w polu tekstowym nie powoduje ponownego łączenia się z czatem. W przeciwieństwie do funkcji, która jest tworzona na nowo, ciąg znaków taki jak roomId
nie zmienia się, chyba że zostanie ustawiony na inną wartość. Dowiedz się więcej o usuwaniu zależności.
Odczytywanie najnowszych właściwości i stanu z efektu
Standardowo, gdy w efekcie czytasz wartość reaktywną, musisz ją dodać jako zależność. To zapewnia, że twój efekt “reaguje” na każdą zmianę tej wartości. Jest to oczekiwane zachowanie dla większości zależności.
Jednakże czasami będziesz chcieć odczytać w efekcie najnowsze właściwości i stan bez “reagowania” na nie. Na przykład, wyobraź sobie, że chcesz zapisać do logów liczbę produktów w koszyku zakupowym dla każdej wizyty na stronie:
function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ Wszystkie zależności zadeklarowane
// ...
}
A co jeśli chcesz zalogować nową wizytę na stronie po każdej zmianie url
, ale nie chcesz tego robić, jeśli zmienia się tylko shoppingCart
? Nie możesz wykluczyć shoppingCart
z zależności bez naruszania zasad reaktywności. Jednakże możesz określić, że nie chcesz, aby pewien fragment kodu “reagował” na zmiany, chociaż jest wywoływany wewnątrz efektu. Zadeklaruj zdarzenie efektu za pomocą hooka useEffectEvent
i przenieś kod odczytujący shoppingCart
do jego wnętrza:
function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ Wszystkie zależności zadeklarowane
// ...
}
Zdarzenia efektu nie są reaktywne i muszą być zawsze pominięte w zależnościach efektu. Dzięki temu możesz umieścić kod niereaktywny (gdzie możesz odczytać najnowszą wartość niektórych właściwości i stanu) w ich wnętrzu. Czytając shoppingCart
wewnątrz onVisit
, zapewnisz, że shoppingCart
nie uruchomi ponownie twojego efektu.
Dowiedz się więcej o tym, jak zdarzenia efektu pozwalają oddzielić kod reaktywny od niereaktywnego.
Wyświetlanie różnych treści na serwerze i kliencie
Jeśli twoja aplikacja korzysta z renderowania na serwerze (zarówno bezpośrednio lub za pomocą frameworka), twój komponent będzie renderowany w dwóch różnych środowiskach. Na serwerze zostanie on wyrenderowany, aby wygenerować początkowy kod HTML. Na kliencie React uruchomi kod renderowania ponownie, aby mógł podpiąć do tego kodu HTML funkcje obsługujące zdarzenia. Dlatego, aby hydratacja działała, wynik początkowego renderowania musi być identyczny na kliencie i na serwerze.
W rzadkich przypadkach może zajść potrzeba, aby wyświetlić inną treść na kliencie. Na przykład, jeśli twoja aplikacja odczytuje pewne dane z localStorage
, to nie jest to możliwe do wykonania na serwerze. Oto jak można to zrealizować:
function MyComponent() {
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
}, []);
if (didMount) {
// ... zwróć JSX używany tylko po stronie klienta ...
} else {
// ... zwróć początkowy kod JSX ...
}
}
Podczas ładowania aplikacji użytkownik zobaczy początkowy wynik renderowania. Następnie, gdy aplikacja zostanie załadowana i ulegnie hydratacji, twój efekt zostanie uruchomiony i ustawi didMount
na true
, co spowoduje przerenderowanie. Następnie zostanie wyświetlony wynik renderowania tylko dla klienta. Efekty nie są uruchamiane na serwerze, dlatego też didMount
było ustawione na false
podczas początkowego renderowania na serwerze.
Stosuj ten wzorzec z rozwagą. Pamiętaj, że użytkownicy z wolnym połączeniem będą widzieć początkową zawartość przez pewien czas - potencjalnie przez wiele sekund - dlatego nie chcemy nagłych zmian w wyglądzie twojego komponentu. W wielu przypadkach można uniknąć konieczności korzystania z tego rozwiązania, wyświetlając warunkowo inne elementy za pomocą CSS.
Znane problemy
Mój efekt jest uruchamiany podwójnie, gdy komponent jest montowany
Kiedy tryb rygorystyczny jest włączony, w trybie deweloperskim React uruchamia dodatkowo funkcje konfigurującą i czyszczącą przed właściwym uruchomieniem funkcji konfigurującej.
Jest to test obciążeniowy, który sprawdza, czy logika twojego efektu jest poprawnie zaimplementowana. Jeśli to powoduje widoczne problemy, oznacza to, że brakuje pewnej logiki w funkcji czyszczącej. Powinna ona zatrzymać lub wycofać to, co robi funkcja konfigurująca. Ogólna zasada jest taka, że użytkownik nie powinien być w stanie rozróżnić między tym, czy konfigurowanie zostało wywołane tylko raz (jak na produkcji), czy też w sekwencji konfigurowanie → czyszczenie → konfigurowanie (jak w trybie deweloperskim).
Dowiedz się więcej o tym, jak to pomaga znajdować błędy oraz jak naprawić logikę swojego efektu.
Mój efekt uruchamia się po każdym przerenderowaniu
Sprawdź najpierw, czy przypadkiem nie brakuje tablicy zależności:
useEffect(() => {
// ...
}); // 🚩 Brak tablicy zależności: efekt uruchamia się po każdym renderowaniu!
Jeśli tablica zależności jest podana, ale twój efekt nadal wywołuje się w pętli, może to być spowodowane tym, że jedna z twoich zależności zmienia się przy każdym przerenderowaniu.
Możesz sprawdzić, czy to jest przyczyną, wypisując zależności do konsoli:
useEffect(() => {
// ..
}, [serverUrl, roomId]);
console.log([serverUrl, roomId]);
Następnie możesz kliknąć prawym przyciskiem myszy na tablicach z różnych przerenderowań w konsoli i wybrać “Zapisz jako zmienną globalną” (ang. Store as Global Variable) dla obu z nich. Zakładając, że pierwsza została zapisana jako temp1
, a druga jako temp2
, możesz użyć konsoli przeglądarki, aby sprawdzić, czy każda zależność w obu tablicach jest taka sama:
Object.is(temp1[0], temp2[0]); // Czy pierwsza zależność jest taka sama w obu tablicach?
Object.is(temp1[1], temp2[1]); // Czy druga zależność jest taka sama w obu tablicach?
Object.is(temp1[2], temp2[2]); // ... i tak dalej dla każdej zależności ...
Kiedy znajdziesz zależność, która zmienia się przy każdym przerenderowaniu, zazwyczaj da się to naprawić jednym z poniższych sposobów:
- Aktualizacja stanu na podstawie poprzedniego stanu z efektu
- Usunięcie zbędnych zależności od obiektów
- Usunięcie zbędnych zależności od funkcji
- Odczytywanie najnowszych właściwości i stanu z efektu
Ostatecznym rozwiązaniem (jeśli powyższe metody nie pomogły) jest opakowanie tworzenia obiektu w useMemo
lub useCallback
(dla funkcji).
Mój efekt wpada w nieskończoną pętlę
Jeżeli twój efekt wpada w nieskończoną pętlę, musi chodzić o następującą sytuację:
- Twój efekt aktualizuje jakiś stan.
- Ten stan prowadzi do przerenderowania, co powoduje zmiany w zależnościach efektu.
Zanim ruszysz do naprawiania problemu, zastanów się, czy twój efekt nie łączy się z jakimś zewnętrznym systemem (takim jak drzewo DOM, sieć, widżet itp.). Dlaczego twój efekt musi ustawiać stan? Czy synchronizuje się on z tym zewnętrznym systemem? Czy próbujesz za jego pomocą zarządzać przepływem danych w twojej aplikacji?
Jeśli nie ma żadnego zewnętrznego systemu, zastanów się, czy całkowite usunięcie efektu nie uprościłoby twojej logiki.
Jeśli rzeczywiście synchronizujesz się z jakimś zewnętrznym systemem, zastanów się, dlaczego i pod jakim warunkiem twój efekt powinien aktualizować stan. Czy zmieniło się coś, co wpływa na to, jak powinien wyglądać twój komponent? Jeśli musisz śledzić jakieś dane, które nie są używane do renderowania, może bardziej odpowiednie będzie użycie referencji (która nie powoduje przerenderowania). Upewnij się, że twój efekt nie aktualizuje stanu (i nie powoduje ponownych renderowań) częściej niż to konieczne.
Kończąc, jeśli twój efekt aktualizuje stan w odpowiednim momencie, ale wciąż występuje zapętlenie, oznacza to, że ta aktualizacja stanu prowadzi do zmiany jednej z zależności efektu. Przeczytaj, jak debugować zmiany w zależnościach.
Moja funkcja czyszcząca jest uruchamiana nawet wtedy, gdy mój komponent nie jest odmontowywany
Funkcja czyszcząca uruchamia się nie tylko podczas odmontowywania, ale również przed każdym przerenderowaniem ze zmienionymi zależnościami. Dodatkowo, w trybie deweloperskim, React uruchamia funkcję konfigurującą oraz czyszczącą dodatkowy raz, tuż po zamontowaniu komponentu.
Jeśli masz kod czyszczący bez odpowiadającego mu kodu konfigurującego, zazwyczaj to on jest przyczyną problemów:
useEffect(() => {
// 🔴 Unikaj: Logika czyszczenia bez odpowiadającej jej logiki konfigurującej.
return () => {
doSomething();
};
}, []);
Twoja logika czyszcząca powinna być “symetryczna” względem logiki konfigurującej i powinna zatrzymać lub wycofać to, co zrobiła funkcja konfigurująca:
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
Dowiedz się, jak cykl życia efektu różni się od cyklu życia komponentu.
Mój efekt robi coś wizualnego i widzę migotanie przed jego uruchomieniem
Jeśli twój efekt musi wstrzymać przeglądarkę przed pokazaniem zawartości na ekranie, zamień useEffect
na useLayoutEffect
. Pamiętaj, że nie jest to konieczne w przypadku zdecydowanej większości efektów. Będziesz tego potrzebować tylko wtedy, gdy konieczne jest uruchomienie efektu przed tym, jak przeglądarka zacznie wyświetlać zawartość, na przykład do pomiaru i pozycjonowania dymka podpowiedzi, zanim użytkownik go zobaczy.