Lekcja 14: Hook useEffect
W poprzedniej lekcji wprowadziliśmy Hooki i ich podstawowe zasady. W lekcji 6 poznaliśmy już Hook useEffect
jako narzędzie do obsługi efektów ubocznych i cyklu życia w komponentach funkcyjnych. W tej lekcji przyjrzymy mu się bliżej, skupiając się na jego zastosowaniach, tablicy zależności i funkcji czyszczącej.
Przypomnienie: Składnia `useEffect`
import React, { useEffect } from \'react\';
useEffect(() => {
// Kod efektu ubocznego (np. fetch, subskrypcja, timer)
console.log(\'Efekt został uruchomiony!\');
// Opcjonalna funkcja czyszcząca
return () => {
console.log(\'Czyszczenie po efekcie...\');
// np. clearInterval(timerId), window.removeEventListener(...)
};
}, [dependency1, dependency2]); // Tablica zależności
- Funkcja efektu: Kod, który ma zostać wykonany. Uruchamiana po renderowaniu i aktualizacji DOM.
- Funkcja czyszcząca (cleanup): Opcjonalna funkcja zwracana przez funkcję efektu. Uruchamiana przed kolejnym wywołaniem efektu lub przed odmontowaniem komponentu. Służy do "sprzątania" po efekcie.
- Tablica zależności: Kontroluje, kiedy efekt ma zostać ponownie uruchomiony.
Zrozumienie Tablicy Zależności
Tablica zależności jest kluczowym elementem kontrolującym zachowanie useEffect
.
- Brak tablicy (
useEffect(() => {...})
): Efekt uruchamia się po każdym renderowaniu komponentu. Używaj ostrożnie, łatwo o nieskończone pętle, jeśli efekt modyfikuje stan. - Pusta tablica (
useEffect(() => {...}, [])
): Efekt uruchamia się tylko raz, po pierwszym renderowaniu (zamontowaniu) komponentu. Funkcja czyszcząca (jeśli istnieje) uruchamia się tylko przy odmontowywaniu. Idealne do jednorazowych inicjalizacji, pobierania danych przy starcie, ustawiania globalnych nasłuchiwaczy. - Tablica z zależnościami (
useEffect(() => {...}, [dep1, dep2])
): Efekt uruchamia się po pierwszym renderowaniu oraz za każdym razem, gdy którakolwiek z wartości w tablicy zależności ulegnie zmianie w porównaniu do poprzedniego renderowania. React porównuje wartości w tablicy za pomocą porównaniaObject.is
.
Ważna zasada: W tablicy zależności powinny znaleźć się wszystkie wartości z zasięgu komponentu (propsy, stan, funkcje zdefiniowane w komponencie), które są używane wewnątrz funkcji efektu i których zmiana powinna spowodować ponowne uruchomienie efektu. Linter (eslint-plugin-react-hooks
) zazwyczaj pomaga wychwycić brakujące zależności.
Przykłady Zastosowań `useEffect`
1. Pobieranie Danych (Fetching Data)
Najczęstsze zastosowanie. Zazwyczaj chcemy pobrać dane raz po zamontowaniu komponentu lub gdy zmieni się jakiś identyfikator (np. userId
).
function UserData({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
console.log(`Pobieram dane dla użytkownika ${userId}`);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
// Tutaj funkcja czyszcząca mogłaby anulować zapytanie (AbortController)
// jeśli userId zmieni się zanim fetch się zakończy.
}, [userId]); // Zależność od userId
if (!user) return <p>Ładowanie...</p>;
return <p>Witaj, {user.name}!</p>;
}
2. Ustawianie Subskrypcji
Subskrypcje (np. do WebSockets, zdarzeń zewnętrznych) muszą być anulowane w funkcji czyszczącej, aby uniknąć wycieków pamięci.
function ChatStatus({ chatId }) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
console.log(`Subskrybuję status dla czatu ${chatId}`);
const subscription = ChatAPI.subscribeToStatus(chatId, (status) => {
setIsOnline(status);
});
// Funkcja czyszcząca - anulowanie subskrypcji
return () => {
console.log(`Anuluję subskrypcję dla czatu ${chatId}`);
ChatAPI.unsubscribeFromStatus(chatId, subscription);
};
}, [chatId]); // Zależność od chatId
if (isOnline === null) return <p>Sprawdzanie statusu...</p>;
return <p>Status czatu: {isOnline ? \'Online\' : \'Offline\'}</p>;
}
3. Ręczna Manipulacja DOM
Chociaż React zazwyczaj zarządza DOM za nas, czasami potrzebujemy bezpośredniego dostępu (np. do integracji z bibliotekami non-React, zarządzania focusem). Używamy do tego `useRef` w połączeniu z `useEffect`.
import React, { useRef, useEffect } from \'react\';
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// Ustaw focus na inpucie po zamontowaniu komponentu
if (inputRef.current) {
inputRef.current.focus();
}
}, []); // Pusta tablica - tylko przy montowaniu
return <input ref={inputRef} type="text" placeholder="Mam focus!" />;
}
4. Ustawianie Timerów
Podobnie jak subskrypcje, timery (setTimeout
, setInterval
) muszą być wyczyszczone.
function DebouncedSearch({ onSearch }) {
const [query, setQuery] = useState(\'\');
useEffect(() => {
// Ustaw timer, aby wywołać onSearch dopiero po 500ms bezczynności
const timerId = setTimeout(() => {
if (query) { // Wywołaj tylko jeśli jest co szukać
onSearch(query);
}
}, 500);
// Funkcja czyszcząca - anuluj poprzedni timer przy każdej zmianie query
return () => {
clearTimeout(timerId);
};
}, [query, onSearch]); // Zależność od query i onSearch
return (
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Szukaj..."
/>
);
}
Pułapki i Dobre Praktyki
- Brakujące zależności: Zawsze upewnij się, że tablica zależności zawiera wszystkie wartości z zewnątrz, które są używane w efekcie. Używaj lintera!
- Zależności zmieniające się przy każdym renderowaniu: Unikaj umieszczania w tablicy zależności obiektów lub tablic tworzonych na nowo przy każdym renderowaniu. Może to prowadzić do nieskończonego wywoływania efektu. Jeśli musisz użyć obiektu/tablicy jako zależności, rozważ użycie
useMemo
lubuseCallback
(omówione później) lub serializacji wartości. - Funkcje jako zależności: Jeśli funkcja zdefiniowana w komponencie jest używana w
useEffect
, powinna znaleźć się w tablicy zależności. Aby uniknąć niepotrzebnego ponownego uruchamiania efektu, gdy funkcja się nie zmieniła, można ją opakować wuseCallback
lub przenieść definicję funkcji do wnętrzauseEffect
(jeśli nie potrzebuje dostępu do propsów/stanu spoza efektu). - Czyszczenie: Zawsze implementuj funkcję czyszczącą, jeśli efekt tworzy subskrypcje, timery, nasłuchiwacze zdarzeń globalnych lub inne zasoby, które mogą powodować wycieki pamięci.
- Warunki wewnątrz efektu: Zamiast warunkowo wywoływać
useEffect
(co jest zabronione), umieść warunek wewnątrz funkcji efektu, aby zdecydować, czy dana operacja ma zostać wykonana.
Ćwiczenie praktyczne
Pokaż rozwiązanie
// src/TitleCounter.jsx
import React, { useState, useEffect } from \'react\';
function TitleCounter() {
const [count, setCount] = useState(0);
const [originalTitle] = useState(document.title); // Zapisz oryginalny tytuł przy montowaniu
// Efekt aktualizujący tytuł dokumentu
useEffect(() => {
console.log(\'Aktualizuję tytuł dokumentu\');
document.title = `Kliknięto ${count} razy`;
// Opcjonalna funkcja czyszcząca - przywraca tytuł przy odmontowywaniu
return () => {
console.log(\'Przywracam oryginalny tytuł\');
document.title = originalTitle;
};
// Zależność od `count`. `originalTitle` jest stały, więc nie musi tu być.
}, [count, originalTitle]);
const increment = () => {
setCount(prevCount => prevCount + 1);
};
return (
<div>
<p>Licznik: {count}</p>
<p>(Sprawdź tytuł karty przeglądarki)</p>
<button onClick={increment}>Kliknij mnie</button>
</div>
);
}
export default TitleCounter;
// W App.jsx
import React from \'react\';
import TitleCounter from \'./TitleCounter\';
function App() {
// Można dodać logikę warunkowego montowania/odmontowywania TitleCounter
// aby przetestować funkcję czyszczącą.
const [showCounter, setShowCounter] = useState(true);
return (
<div>
<button onClick={() => setShowCounter(!showCounter)}>
{showCounter ? \'Ukryj\' : \'Pokaż\'} Licznik Tytułu
</button>
{showCounter && <TitleCounter />}
</div>
);
}
export default App;
Cel: Stworzyć komponent, który zmienia tytuł dokumentu (document.title
) na podstawie wartości licznika.
Kroki:
- Stwórz komponent funkcyjny
TitleCounter
. - Użyj
useState
do przechowywania wartości licznika (count
), zainicjalizowanego na 0. - Dodaj przycisk, który inkrementuje licznik po kliknięciu.
- Użyj
useEffect
, aby zaktualizowaćdocument.title
za każdym razem, gdy zmieni się wartośćcount
. Tytuł powinien wyglądać np. "Kliknięto X razy". - Upewnij się, że
count
jest w tablicy zależnościuseEffect
. - Opcjonalnie: Dodaj funkcję czyszczącą, która przywraca oryginalny tytuł dokumentu, gdy komponent jest odmontowywany.
- Wyświetl wartość licznika i przycisk w komponencie.
- Użyj
TitleCounter
wApp.jsx
.
Zadanie do samodzielnego wykonania
Stwórz komponent FetchPosts
, który:
- Pobiera listę postów z publicznego API (np.
https://jsonplaceholder.typicode.com/posts
) po zamontowaniu komponentu. - Przechowuje pobrane posty w stanie.
- Wyświetla stan ładowania ("Ładowanie postów...") podczas pobierania.
- Wyświetla listę tytułów postów (
post.title
), gdy dane zostaną załadowane (użyjmap
i kluczy). - Obsługuje potencjalny błąd podczas pobierania i wyświetla komunikat o błędzie.
- Użyj
useEffect
z pustą tablicą zależności do jednorazowego pobrania danych.
FAQ - Hook useEffect
Czy mogę mieć wiele `useEffect` w jednym komponencie?
Tak, jest to nawet zalecane. Możesz używać wielu `useEffect` do rozdzielenia niezależnych od siebie efektów ubocznych. Na przykład jeden `useEffect` do pobierania danych, a drugi do ustawiania subskrypcji. Każdy z nich będzie miał własną logikę, tablicę zależności i funkcję czyszczącą.
Kiedy dokładnie uruchamiana jest funkcja czyszcząca?
Funkcja czyszcząca jest uruchamiana: 1) Tuż przed wykonaniem kolejnego efektu (jeśli zależności się zmieniły i efekt ma zostać uruchomiony ponownie). 2) Tuż przed odmontowaniem komponentu. Służy do posprzątania po *poprzednim* efekcie.
Co jeśli mój efekt nie potrzebuje czyszczenia?
Wtedy po prostu nie zwracasz żadnej funkcji z funkcji efektu. Jest to całkowicie poprawne. Czyszczenie jest potrzebne tylko wtedy, gdy efekt tworzy zasoby (subskrypcje, timery, nasłuchiwacze), które trzeba zwolnić.
Jak radzić sobie z funkcjami w tablicy zależności `useEffect`?
Jeśli funkcja jest zdefiniowana wewnątrz komponentu i używana w `useEffect`, powinna znaleźć się w zależnościach. Aby uniknąć jej zmiany przy każdym renderowaniu (co powodowałoby ponowne uruchomienie efektu), można: a) opakować ją w `useCallback`, b) przenieść jej definicję do wnętrza `useEffect` (jeśli nie zależy od propsów/stanu), c) przenieść ją poza komponent (jeśli jest czystą funkcją).
Czy `useEffect` z pustą tablicą `[]` jest dokładnie tym samym co `componentDidMount`?
Prawie. Oba uruchamiają się raz po pierwszym renderowaniu. Różnica polega na tym, że funkcja czyszcząca zwrócona z `useEffect(..., [])` działa jak `componentWillUnmount`. `useEffect` jest bardziej zorientowany na synchronizację z propsami i stanem niż na konkretne momenty cyklu życia.
Jak mogę uruchomić efekt tylko przy aktualizacji, a nie przy pierwszym montowaniu?
Nie ma wbudowanego sposobu, aby `useEffect` pominął pierwsze renderowanie. Popularnym obejściem jest użycie `useRef` do śledzenia, czy komponent został już zamontowany:
Czy `useEffect` może być asynchroniczny (`async`)?
Funkcja przekazywana bezpośrednio do `useEffect` nie powinna być `async`, ponieważ `useEffect` oczekuje, że zwróci ona funkcję czyszczącą (lub nic), a nie Promise. Można jednak zdefiniować i wywołać funkcję `async` wewnątrz `useEffect`: `useEffect(() => { const fetchData = async () => { ... }; fetchData(); }, []);`.