Lekcja 6: Cykl Życia Komponentu - Hook useEffect

Komponenty React przechodzą przez różne fazy podczas swojego istnienia na stronie: montowanie (mounting), aktualizacja (updating) i odmontowywanie (unmounting). W komponentach klasowych mieliśmy dedykowane metody cyklu życia (np. componentDidMount, componentDidUpdate, componentWillUnmount). W komponentach funkcyjnych, do obsługi tych faz oraz zarządzania efektami ubocznymi (side effects) służy Hook useEffect.

Co to są Efekty Uboczne?

Efekty uboczne to operacje, które komponent musi wykonać poza swoim głównym zadaniem, jakim jest renderowanie UI. Przykłady efektów ubocznych to:

Takie operacje nie powinny być wykonywane bezpośrednio w ciele komponentu funkcyjnego, ponieważ ciało funkcji jest wykonywane przy każdym renderowaniu, co mogłoby prowadzić do niekontrolowanego powtarzania efektów.

Hook `useEffect`

Hook useEffect pozwala uruchamiać efekty uboczne w odpowiedzi na zmiany w cyklu życia komponentu lub zmiany określonych wartości (stanu lub propsów).

Podstawowa składnia:**

import React, { useState, useEffect } from \'react\';

function MyComponent(props) {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // Kod efektu ubocznego
    console.log(\'Komponent został zamontowany lub zaktualizowany.\');
    document.title = `Kliknięto ${count} razy`;

    // Opcjonalna funkcja czyszcząca (cleanup function)
    return () => {
      console.log(\'Czyszczenie przed kolejnym uruchomieniem efektu lub odmontowaniem komponentu.\');
      // np. anulowanie subskrypcji, czyszczenie timerów
    };
  }, [dependencies]); // Tablica zależności

  // ... reszta komponentu
}

Kontrolowanie Uruchamiania Efektu za pomocą Tablicy Zależności

1. Efekt uruchamiany po każdym renderowaniu

Jeśli nie podasz tablicy zależności, efekt będzie uruchamiany po każdym renderowaniu komponentu (zarówno początkowym, jak i po każdej aktualizacji).

useEffect(() => {
  console.log(\'Uruchamiam po każdym renderowaniu!\');
}); // Brak tablicy zależności

Uwaga: Używaj tego ostrożnie, ponieważ może to prowadzić do problemów z wydajnością lub nieskończonych pętli, jeśli efekt sam powoduje zmianę stanu.

2. Efekt uruchamiany tylko raz (po zamontowaniu)

Jeśli podasz pustą tablicę zależności [], efekt zostanie uruchomiony tylko raz, zaraz po pierwszym renderowaniu komponentu (podobnie do componentDidMount).

useEffect(() => {
  console.log(\'Komponent zamontowany!\');
  // Idealne miejsce na jednorazowe pobranie danych z API
}, []); // Pusta tablica zależności

3. Efekt uruchamiany, gdy zmienią się zależności

Jeśli podasz tablicę z wartościami (zazwyczaj propsy lub stan), efekt zostanie uruchomiony 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.

const [userId, setUserId] = useState(1);

useEffect(() => {
  console.log(`Pobieram dane dla użytkownika ${userId}`);
  // fetch(`/api/users/${userId}`)... 
}, [userId]); // Efekt uruchomi się, gdy zmieni się `userId`

Ważne: W tablicy zależności umieszczaj tylko te wartości (propsy, stan, funkcje zdefiniowane w komponencie), które są używane wewnątrz funkcji efektu i których zmiana powinna spowodować ponowne uruchomienie efektu.

Funkcja Czyszcząca (Cleanup Function)

Funkcja zwracana przez funkcję efektu jest uruchamiana:

Jest to idealne miejsce do "posprzątania" po efekcie, np. anulowania subskrypcji, usunięcia nasłuchiwania zdarzeń, wyczyszczenia timerów.

useEffect(() => {
  const handleResize = () => console.log(\'Zmieniono rozmiar okna\');
  window.addEventListener(\'resize\', handleResize);
  console.log(\'Dodano nasłuchiwanie resize\');

  // Funkcja czyszcząca
  return () => {
    window.removeEventListener(\'resize\', handleResize);
    console.log(\'Usunięto nasłuchiwanie resize\');
  };
}, []); // Uruchom tylko raz przy montowaniu, wyczyść przy odmontowywaniu

Przykład: Pobieranie Danych z API

useEffect jest powszechnie używany do pobierania danych po zamontowaniu komponentu.

import React, { useState, useEffect } from \'react\';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true); // Rozpocznij ładowanie
    setError(null);   // Zresetuj błąd

    fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
      .then(response => {
        if (!response.ok) {
          throw new Error(\'Nie udało się pobrać danych\');
        }
        return response.json();
      })
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(error => {
        setError(error.message);
        setLoading(false);
      });

    // Funkcja czyszcząca nie jest tu zazwyczaj potrzebna, 
    // chyba że chcemy anulować zapytanie, jeśli komponent zostanie odmontowany
    // przed otrzymaniem odpowiedzi (np. za pomocą AbortController)

  }, [userId]); // Pobierz dane ponownie, gdy zmieni się userId

  if (loading) return <p>Ładowanie...</p>;
  if (error) return <p>Błąd: {error}</p>;
  if (!user) return null; // Lub inny komunikat

  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
      <p>Telefon: {user.phone}</p>
    </div>
  );
}

export default UserProfile;

Ćwiczenie praktyczne

Pokaż rozwiązanie
// src/Clock.jsx
import React, { useState, useEffect } from \'react\';

function Clock() {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    // Uruchom timer po zamontowaniu
    const timerId = setInterval(() => {
      setTime(new Date());
    }, 1000);

    console.log(\'Timer uruchomiony\');

    // Funkcja czyszcząca - uruchamiana przy odmontowywaniu
    return () => {
      clearInterval(timerId);
      console.log(\'Timer zatrzymany\');
    };
  }, []); // Pusta tablica zależności - uruchom efekt tylko raz

  return (
    <div>
      <h2>Aktualny czas:</h2>
      <p>{time.toLocaleTimeString()}</p>
    </div>
  );
}

export default Clock;

// W App.jsx
import React from \'react\';
import Clock from \'./Clock\";

function App() {
  return (
    <div>
      <h1>Zegar React</h1>
      <Clock />
    </div>
  );
}

export default App;

Cel: Stworzyć komponent, który co sekundę aktualizuje wyświetlany czas.

Kroki:

  1. Stwórz komponent funkcyjny Clock.
  2. Użyj useState do przechowywania aktualnego czasu (obiektu Date).
  3. Użyj useEffect, aby uruchomić timer (setInterval) po zamontowaniu komponentu. Timer powinien co sekundę aktualizować stan czasu na nowy obiekt Date.
  4. Wewnątrz useEffect zwróć funkcję czyszczącą, która anuluje timer (clearInterval), gdy komponent zostanie odmontowany.
  5. Wyrenderuj aktualny czas, formatując go np. za pomocą toLocaleTimeString().
  6. Użyj komponentu Clock w App.jsx.

Zadanie do samodzielnego wykonania

Stwórz komponent WindowWidthLogger, który:

  1. Wyświetla aktualną szerokość okna przeglądarki.
  2. Używa useState do przechowywania szerokości okna.
  3. Używa useEffect do dodania nasłuchiwania zdarzenia resize na obiekcie window po zamontowaniu komponentu.
  4. W funkcji obsługi zdarzenia resize aktualizuje stan szerokości okna.
  5. Implementuje funkcję czyszczącą w useEffect, która usuwa nasłuchiwanie zdarzenia resize przy odmontowywaniu komponentu.
  6. Początkową szerokość okna ustawia w stanie przy pierwszym renderowaniu (można to zrobić w useState lub w useEffect).

FAQ - Cykl Życia Komponentu - Hook useEffect

Czy `useEffect` zastępuje wszystkie metody cyklu życia komponentów klasowych?

Tak, `useEffect` pozwala osiągnąć te same cele co `componentDidMount`, `componentDidUpdate` i `componentWillUnmount`, ale w bardziej zunifikowany sposób. Sposób działania `useEffect` zależy od jego tablicy zależności. Pusta tablica `[]` naśladuje `componentDidMount` i `componentWillUnmount` (dla cleanup). Tablica z zależnościami naśladuje `componentDidUpdate`.

Co jeśli zapomnę dodać zależność do tablicy zależności?

Jeśli funkcja efektu używa wartości (props, stan, funkcja), która może się zmieniać, ale nie jest uwzględniona w tablicy zależności, efekt może działać z nieaktualnymi danymi (stale closure). Narzędzia takie jak ESLint (z wtyczką `eslint-plugin-react-hooks`) pomagają wykrywać brakujące zależności.

Czy mogę mieć wiele `useEffect` w jednym komponencie?

Tak, możesz używać `useEffect` wielokrotnie w jednym komponencie. Jest to nawet zalecane, aby rozdzielić niezależne od siebie efekty uboczne. Każdy `useEffect` może mieć własną logikę, tablicę zależności i funkcję czyszczącą.

Kiedy dokładnie uruchamiana jest funkcja `useEffect`?

Funkcja `useEffect` jest uruchamiana *po* zakończeniu renderowania i aktualizacji DOM przez React. Dzięki temu kod efektu nie blokuje renderowania interfejsu, co poprawia wrażenia użytkownika. Funkcja czyszcząca jest uruchamiana tuż przed następnym uruchomieniem efektu lub przed odmontowaniem.

Czy mogę używać `async/await` w `useEffect`?

Funkcja przekazywana bezpośrednio do `useEffect` nie powinna być funkcją `async`, ponieważ `useEffect` oczekuje, że zwrócona wartość będzie funkcją czyszczącą (lub niczym). Można jednak zdefiniować i wywołać funkcję `async` wewnątrz `useEffect`:

Jak uniknąć nieskończonej pętli z `useEffect`?

Najczęstszą przyczyną nieskończonych pętli jest aktualizowanie stanu wewnątrz `useEffect` bez odpowiedniej tablicy zależności lub gdy zależność zmienia się przy każdym renderowaniu (np. obiekt lub tablica tworzona na nowo). Upewnij się, że tablica zależności zawiera tylko te wartości, które faktycznie powinny wyzwalać efekt, i unikaj tworzenia nowych obiektów/tablic jako zależności przy każdym renderowaniu.

Czy funkcja czyszcząca jest zawsze potrzebna?

Nie, funkcja czyszcząca jest potrzebna tylko wtedy, gdy efekt uboczny tworzy coś, co wymaga "posprzątania" przed odmontowaniem komponentu lub ponownym uruchomieniem efektu. Przykłady to subskrypcje, timery, nasłuchiwanie zdarzeń globalnych. Dla jednorazowych operacji, jak pobranie danych, często nie jest konieczna.