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

Zrozumienie Tablicy Zależności

Tablica zależności jest kluczowym elementem kontrolującym zachowanie useEffect.

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


Ć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:

  1. Stwórz komponent funkcyjny TitleCounter.
  2. Użyj useState do przechowywania wartości licznika (count), zainicjalizowanego na 0.
  3. Dodaj przycisk, który inkrementuje licznik po kliknięciu.
  4. 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".
  5. Upewnij się, że count jest w tablicy zależności useEffect.
  6. Opcjonalnie: Dodaj funkcję czyszczącą, która przywraca oryginalny tytuł dokumentu, gdy komponent jest odmontowywany.
  7. Wyświetl wartość licznika i przycisk w komponencie.
  8. Użyj TitleCounter w App.jsx.

Zadanie do samodzielnego wykonania

Stwórz komponent FetchPosts, który:

  1. Pobiera listę postów z publicznego API (np. https://jsonplaceholder.typicode.com/posts) po zamontowaniu komponentu.
  2. Przechowuje pobrane posty w stanie.
  3. Wyświetla stan ładowania ("Ładowanie postów...") podczas pobierania.
  4. Wyświetla listę tytułów postów (post.title), gdy dane zostaną załadowane (użyj map i kluczy).
  5. Obsługuje potencjalny błąd podczas pobierania i wyświetla komunikat o błędzie.
  6. 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(); }, []);`.