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:
- Reużycie logiki stanowej: Wcześniej reużywanie logiki związanej ze stanem między komponentami było trudne (wymagało np. render props lub HOC - Higher-Order Components), co prowadziło do tzw. "wrapper hell". Hooki (zwłaszcza niestandardowe) rozwiązują ten problem w elegancki sposób.
- Złożone komponenty stają się trudne do zrozumienia: Komponenty klasowe często rozrastały się, a logika związana z różnymi aspektami cyklu życia (np. pobieranie danych, subskrypcje) była rozproszona po różnych metodach (
componentDidMount
,componentDidUpdate
,componentWillUnmount
). HookuseEffect
pozwala grupować powiązaną logikę. - Klasy są mylące dla ludzi i maszyn: Składnia klas w JavaScript, działanie
this
, konieczność bindowania metod - to wszystko stanowiło barierę wejścia i mogło prowadzić do błędów. Komponenty funkcyjne z Hookami są postrzegane jako prostsze.
Wbudowane Hooki
React dostarcza zestaw wbudowanych Hooków. Poznaliśmy już dwa najważniejsze:
useState
: Pozwala na dodanie lokalnego stanu do komponentu funkcyjnego (Lekcja 5).useEffect
: Pozwala na wykonywanie efektów ubocznych (np. pobieranie danych, subskrypcje, manipulacja DOM) w komponencie funkcyjnym (Lekcja 6).
Inne wbudowane Hooki, które omówimy w kolejnych lekcjach, to między innymi:
useContext
: Pozwala na subskrybowanie kontekstu Reacta bez wprowadzania zagnieżdżenia (alternatywa dla Context Consumer).useReducer
: Alternatywa dlauseState
, przydatna do zarządzania bardziej złożonym stanem z wykorzystaniem wzorca reduktora (podobnego do Redux).useCallback
: Zwraca zmemoizowaną wersję funkcji zwrotnej, która zmienia się tylko wtedy, gdy zmienią się jej zależności. Przydatne do optymalizacji.useMemo
: Zwraca zmemoizowaną wartość. Oblicza wartość tylko wtedy, gdy zmienią się jej zależności. Przydatne do optymalizacji kosztownych obliczeń.useRef
: Zwraca mutowalny obiekt referencji, którego właściwość.current
może przechowywać dowolną wartość. Przydatne do uzyskiwania dostępu do elementów DOM lub przechowywania wartości, które nie powodują ponownego renderowania.useImperativeHandle
,useLayoutEffect
,useDebugValue
: Bardziej zaawansowane Hooki do specyficznych zastosowań.
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ł
- Przewidywalność: Kod staje się łatwiejszy do zrozumienia i debugowania, ponieważ stan i efekty są zawsze powiązane z komponentem w ten sam sposób.
- Automatyzacja: Narzędzia takie jak lintery mogą automatycznie wykrywać naruszenia reguł, zapobiegając błędom.
- Optymalizacje: Przestrzeganie reguł pozwala Reactowi na stosowanie optymalizacji.
Ć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:
- Zidentyfikuj wszystkie miejsca, gdzie naruszono reguły Hooków.
- 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 zmiennejdata
, 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ń.