Lekcja 13: Hooki (Hooks) - Wprowadzenie i Reguły

Od wersji React 16.8, Hooki zrewolucjonizowały sposób pisania komponentów. Pozwalają one na używanie stanu, efektów ubocznych i innych funkcji Reacta w komponentach funkcyjnych, eliminując w dużej mierze potrzebę pisania komponentów klasowych. W tej lekcji wprowadzimy koncepcję Hooków i poznamy ich fundamentalne zasady.

Czym są Hooki?

Hooki to specjalne funkcje JavaScript, które pozwalają "zahaczyć" (ang. *hook into*) o mechanizmy Reacta, takie jak stan i cykl życia, z poziomu komponentów funkcyjnych. Dzięki nim komponenty funkcyjne stały się równie potężne co komponenty klasowe, a jednocześnie często prostsze i bardziej czytelne.

Motywacja powstania Hooków:

Wbudowane Hooki

React dostarcza zestaw wbudowanych Hooków. Poznaliśmy już dwa najważniejsze:

Inne wbudowane Hooki, które omówimy w kolejnych lekcjach, to między innymi:

Reguły Używania Hooków

Aby Hooki działały poprawnie, React narzuca dwie kluczowe reguły. Narzędzia takie jak ESLint (z wtyczką eslint-plugin-react-hooks) pomagają w ich egzekwowaniu.

Reguła 1: Wywołuj Hooki tylko na Najwyższym Poziomie

Nie wywołuj Hooków wewnątrz pętli, warunków (if) ani zagnieżdżonych funkcji. Hooki muszą być zawsze wywoływane w tej samej kolejności przy każdym renderowaniu komponentu.

// ŹLE 🔴
function MyComponent({ condition }) {
  if (condition) {
    // To jest niedozwolone! Hook wywoływany warunkowo.
    const [state, setState] = useState(0);
  }
  
  useEffect(() => { // Kolejność tego Hooka zależy od warunku
    // ...
  });
}

// DOBRZE ✅
function MyComponent({ condition }) {
  // Hooki wywoływane zawsze na najwyższym poziomie
  const [state, setState] = useState(0);
  
  useEffect(() => {
    // Warunek można umieścić WEWNĄTRZ Hooka
    if (condition) {
      // ... wykonaj efekt tylko gdy warunek jest spełniony
    }
  });

  let content = null;
  if (condition) {
      content = <p>Stan: {state}</p>;
  }

  return <div>{content}</div>;
}

Dlaczego ta reguła istnieje? React polega na kolejności wywołań Hooków, aby poprawnie powiązać stan i efekty z konkretnym komponentem między renderowaniami. Wywołanie warunkowe zaburzyłoby tę kolejność.

Reguła 2: Wywołuj Hooki tylko z Komponentów Funkcyjnych React lub Niestandardowych Hooków

Nie wywołuj Hooków ze zwykłych funkcji JavaScript ani z komponentów klasowych.

// ŹLE 🔴
function regularFunction() {
  // To jest niedozwolone! Zwykła funkcja JS.
  const [value, setValue] = useState("test"); 
}

class MyClassComponent extends React.Component {
  render() {
    // To jest niedozwolone! Komponent klasowy.
    const [value, setValue] = useState("test"); 
    return <div>...</div>;
  }
}

// DOBRZE ✅
function MyFunctionalComponent() {
  // Hooki wywoływane w komponencie funkcyjnym.
  const [value, setValue] = useState("test"); 
  return <div>{value}</div>;
}

// DOBRZE ✅ (Niestandardowy Hook - omówimy w kolejnej lekcji)
function useMyCustomHook() {
  // Hooki wywoływane w niestandardowym Hooku.
  const [value, setValue] = useState("test"); 
  useEffect(() => { /* ... */ });
  return value;
}

Dlaczego ta reguła istnieje? Hooki są fundamentalnie powiązane z mechanizmami renderowania i stanu komponentów React. Wywołanie ich poza tym kontekstem nie ma sensu i nie zadziała.

Korzyści z Przestrzegania Reguł


Ćwiczenie praktyczne

Pokaż rozwiązanie
import React, { useState, useEffect } from \'react\';

function GoodComponent({ id }) {
  // Hooki wywołane na najwyższym poziomie
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false); // Początkowo nie ładujemy
  const [clickCount, setClickCount] = useState(0); // Stan dla licznika kliknięć

  // Efekt do pobierania danych
  useEffect(() => {
    // Uruchom efekt tylko jeśli `id` istnieje
    if (id) {
      setLoading(true); // Rozpocznij ładowanie
      setData(null); // Zresetuj dane
      fetch(`/api/data/${id}`) // Załóżmy, że to API istnieje
        .then(res => res.ok ? res.json() : Promise.reject(\'Błąd pobierania\'))
        .then(fetchedData => {
          setData(fetchedData); // Ustaw pobrane dane w stanie
          setLoading(false);
        })
        .catch(error => {
            console.error("Błąd fetch:", error);
            setLoading(false); // Zakończ ładowanie nawet przy błędzie
        });
    } else {
        // Jeśli nie ma id, zresetuj stan
        setData(null);
        setLoading(false);
    }
  }, [id]); // Zależność od `id`

  // Handler kliknięcia - aktualizuje stan `clickCount`
  const handleClick = () => {
    setClickCount(prevCount => prevCount + 1);
  };

  // Renderowanie warunkowe na podstawie stanu
  let content;
  if (!id) {
      content = <p>Proszę podać ID.</p>;
  } else if (loading) {
    content = <p>Ładowanie dla ID: {id}...</p>;
  } else if (data) {
    content = <pre>{JSON.stringify(data, null, 2)}</pre>;
  } else {
      content = <p>Nie udało się załadować danych lub brak danych.</p>;
  }

  return (
    <div>
      <h1>Dane:</h1>
      {content}
      <button onClick={handleClick}>Kliknięto {clickCount} razy</button>
    </div>
  );
}

export default GoodComponent;

Cel: Zidentyfikować i poprawić błędy w użyciu Hooków w podanym kodzie.

Kod z błędami:

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

function BadComponent({ id }) {
  let data = null;

  if (id) {
    // BŁĄD 1: Hook useState wywołany warunkowo
    const [loading, setLoading] = useState(true);

    // BŁĄD 2: Hook useEffect wywołany warunkowo
    useEffect(() => {
      setLoading(true);
      fetch(`/api/data/${id}`)
        .then(res => res.json())
        .then(fetchedData => {
          // BŁĄD 3: Próba przypisania do zmiennej spoza Hooka?
          // (To nie zadziała zgodnie z oczekiwaniami, 
          //  bo renderowanie już się zakończyło)
          data = fetchedData; 
          setLoading(false);
        });
    }, [id]);

    if (loading) {
      return <p>Ładowanie dla ID: {id}...</p>;
    }
  }

  function handleClick() {
      // BŁĄD 4: Hook useState wywołany wewnątrz zwykłej funkcji
      const [count, setCount] = useState(0);
      setCount(c => c + 1);
      console.log("Kliknięto", count);
  }

  return (
    <div>
      <h1>Dane:</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
      <button onClick={handleClick}>Kliknij (błędny hook)</button>
    </div>
  );
}

export default BadComponent;

Kroki:

  1. Zidentyfikuj wszystkie miejsca, gdzie naruszono reguły Hooków.
  2. Przepisz komponent BadComponent tak, aby działał poprawnie, przestrzegając reguł Hooków. Przenieś wywołania Hooków na najwyższy poziom. Zamiast przypisywać do zmiennej data, użyj stanu do przechowywania pobranych danych. Popraw obsługę kliknięcia.

Zadanie do samodzielnego wykonania

Przeanalizuj poniższy kod i wskaż, czy narusza on reguły Hooków. Jeśli tak, wyjaśnij dlaczego i jak można go poprawić.

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

function TimerComponent({ startImmediately }) {
  const [seconds, setSeconds] = useState(0);

  if (startImmediately) {
    useEffect(() => {
      const intervalId = setInterval(() => {
        setSeconds(s => s + 1);
      }, 1000);
      return () => clearInterval(intervalId);
    }, []); // Pusta tablica - czy to na pewno dobrze w tym kontekście?
  }

  return (
    <div>
      <p>Minęło sekund: {seconds}</p>
    </div>
  );
}

FAQ - Hooki (Hooks) - Wprowadzenie i Reguły

Czy Hooki działają w komponentach klasowych?

Nie, Hooki zostały zaprojektowane do użytku wyłącznie w komponentach funkcyjnych React oraz w niestandardowych Hookach. Nie można ich używać wewnątrz komponentów klasowych.

Dlaczego React polega na kolejności wywołań Hooków?

React przechowuje stan i informacje o efektach dla każdego komponentu w wewnętrznej strukturze danych. Przy każdym renderowaniu, React przechodzi przez wywołania Hooków w komponencie i dopasowuje je do zapisanych danych na podstawie ich kolejności. Zmiana kolejności (np. przez warunkowe wywołanie) uniemożliwiłaby to dopasowanie.

Co się stanie, jeśli zignoruję reguły Hooków?

Ignorowanie reguł może prowadzić do trudnych do zdiagnozowania błędów, nieprzewidywalnego zachowania aplikacji, problemów z aktualizacją stanu, błędów podczas renderowania lub niepoprawnego działania efektów ubocznych. Linter (ESLint z odpowiednią wtyczką) jest bardzo pomocny w wykrywaniu tych problemów na wczesnym etapie.

Czy nazwa Hooka musi zaczynać się od "use"?

Tak, to jest konwencja wymagana przez React i narzędzia (np. linter). Wszystkie wbudowane Hooki zaczynają się od "use". Gdy tworzymy własne, niestandardowe Hooki (custom hooks), również musimy nazywać je z prefiksem "use", aby React i linter mogły je poprawnie rozpoznać i zastosować do nich reguły Hooków.

Czy Hooki sprawiają, że komponenty funkcyjne są wolniejsze od klasowych?

Nie. W wielu przypadkach komponenty funkcyjne z Hookami mogą być nawet bardziej wydajne dzięki lepszemu mechanizmowi memoizacji i potencjalnie mniejszemu narzutowi w porównaniu do klas. Wydajność zależy głównie od implementacji komponentu, a nie od tego, czy jest on funkcyjny czy klasowy.

Czy muszę znać komponenty klasowe, aby uczyć się Reacta?

Obecnie nie jest to konieczne. Hooki pozwalają na tworzenie pełnoprawnych aplikacji React przy użyciu wyłącznie komponentów funkcyjnych. Chociaż znajomość komponentów klasowych może być przydatna przy pracy ze starszym kodem, dla nowych projektów i nauki od podstaw można skupić się na komponentach funkcyjnych i Hookach.

Gdzie mogę znaleźć więcej informacji o Hookach?

Najlepszym źródłem jest oficjalna dokumentacja Reacta, która szczegółowo opisuje wszystkie wbudowane Hooki, ich API, reguły użycia oraz motywację ich powstania. Zawiera również wiele przykładów i wyjaśnień.