Kurs JavaScript i React · Moduł 5: useEffect i własne hooki
Tworzenie własnych hooków (Custom Hooks) - zaawansowane wzorce
W tej lekcji7
W kosmicznej flocie React, jednym z najcenniejszych umiejętności inżyniera jest projektowanie modułów wielokrotnego użytku. Wyobraź sobie, że budujesz systemy nawigacyjne, komunikacyjne i diagnostyczne dla setek statków kosmicznych. Zamiast pisać każdy system od zera dla każdego statku, tworzysz uniwersalne moduły, które można podłączyć do dowolnego pojazdu. Dokładnie tak działają Custom Hooks w React.
W poprzedniej lekcji poznaliśmy podstawy custom hooków - useFetch, useInterval, useLocalStorage i useToggle. Teraz zagłębimy się w zaawansowane wzorce, reguły hooków i kompozycję, które uczynią Twoje hooki naprawdę profesjonalnymi.
Reguły Hooków (Rules of Hooks)
Zanim zaczniemy tworzyć zaawansowane hooki, musimy znać dwie fundamentalne reguły, których złamanie powoduje nieprzewidywalne błędy:
Reguła 1: Wywoływanie hooków tylko na najwyższym poziomie
Hooki muszą być wywoływane zawsze w tej samej kolejności przy każdym renderowaniu. Nigdy nie umieszczaj hooków wewnątrz warunków, pętli ani zagnieżdżonych funkcji:
1// ŹLE - hook wewnątrz warunku
2function SpaceShip({ hasRadar }) {
3 if (hasRadar) {
4 const [signal, setSignal] = useState(null); // BŁĄD!
5 }
6}
7
8// DOBRZE - warunek wewnątrz hooka
9function SpaceShip({ hasRadar }) {
10 const [signal, setSignal] = useState(null);
11
12 useEffect(() => {
13 if (hasRadar) {
14 // logika radaru
15 }
16 }, [hasRadar]);
17}React śledzi hooki na podstawie ich kolejności wywołania. Jeśli warunek spowoduje pominięcie jednego hooka, wszystkie następne będą miały błędne wartości - jak gdyby przesunęły się wszystkie moduły w statku kosmicznym.
Reguła 2: Wywoływanie hooków tylko z komponentów React lub custom hooków
Nie możesz wywoływać hooków z zwykłych funkcji JavaScript. Tylko:
- Komponenty funkcyjne React
- Custom Hooki (funkcje zaczynające się od
use)
1// ŹLE - zwykła funkcja
2function calculateTrajectory() {
3 const [speed] = useState(0); // BŁĄD!
4}
5
6// DOBRZE - custom hook
7function useTrajectory() {
8 const [speed, setSpeed] = useState(0);
9 return { speed, setSpeed };
10}useDebounce - opóźnianie akcji
Hook useDebounce jest niezwykle przydatny do wyszukiwania w czasie rzeczywistym. Zamiast wysyłać zapytanie do API po każdym naciśnięciu klawisza, czekamy aż użytkownik skończy pisać:
1function useDebounce(value, delay) {
2 const [debouncedValue, setDebouncedValue] = useState(value);
3
4 useEffect(() => {
5 const timer = setTimeout(() => {
6 setDebouncedValue(value);
7 }, delay);
8
9 return () => clearTimeout(timer);
10 }, [value, delay]);
11
12 return debouncedValue;
13}
14
15// Użycie - wyszukiwanie planet
16function PlanetSearch() {
17 const [query, setQuery] = useState('');
18 const debouncedQuery = useDebounce(query, 500);
19
20 useEffect(() => {
21 if (debouncedQuery) {
22 // Zapytanie wysyłane dopiero po 500ms przerwy w pisaniu
23 fetch(`/api/planets?search=${debouncedQuery}`);
24 }
25 }, [debouncedQuery]);
26
27 return (
28 <input
29 value={query}
30 onChange={e => setQuery(e.target.value)}
31 placeholder="Szukaj planet..."
32 />
33 );
34}Timer jest czyszczony przy każdej zmianie wartości. Dopiero gdy użytkownik przestanie pisać na 500ms, wartość zostanie zaktualizowana i zapytanie API wysłane.
usePrevious - zapamiętywanie poprzedniej wartości
Ten prosty hook pozwala porównywać aktualną wartość z poprzednią:
1function usePrevious(value) {
2 const ref = useRef();
3
4 useEffect(() => {
5 ref.current = value;
6 }, [value]);
7
8 return ref.current;
9}
10
11// Użycie - monitorowanie zmian prędkości
12function SpeedMonitor() {
13 const [speed, setSpeed] = useState(0);
14 const previousSpeed = usePrevious(speed);
15
16 const trend = speed > previousSpeed
17 ? 'Przyspieszanie'
18 : speed < previousSpeed
19 ? 'Hamowanie'
20 : 'Stała prędkość';
21
22 return <p>{trend}: {speed} km/s (było: {previousSpeed})</p>;
23}useArray - zarządzanie tablicami
Tablice wymagają specyficznych operacji (dodawanie, usuwanie, filtrowanie). Custom hook zamyka te operacje w czytelny interfejs:
1function useArray(initialArray = []) {
2 const [array, setArray] = useState(initialArray);
3
4 const push = (element) => setArray(a => [...a, element]);
5 const removeByIndex = (index) =>
6 setArray(a => a.filter((_, i) => i !== index));
7 const update = (index, newElement) =>
8 setArray(a => a.map((item, i) => (i === index ? newElement : item)));
9 const clear = () => setArray([]);
10 const filter = (fn) => setArray(a => a.filter(fn));
11
12 return { array, set: setArray, push, removeByIndex, update, clear, filter };
13}
14
15// Użycie - lista załogantów
16function CrewManager() {
17 const crew = useArray(['Kapitan Nova', 'Nawigator Rex']);
18
19 return (
20 <div>
21 {crew.array.map((member, i) => (
22 <div key={i}>
23 {member}
24 <button onClick={() => crew.removeByIndex(i)}>Usuń</button>
25 </div>
26 ))}
27 <button onClick={() => crew.push('Nowy członek')}>
28 Dodaj członka załogi
29 </button>
30 <button onClick={crew.clear}>Zwolnij załogę</button>
31 </div>
32 );
33}Kompozycja hooków - łączenie modułów
Prawdziwa siła custom hooków ujawnia się, gdy łączymy je ze sobą. Hooki mogą wywoływać inne hooki, tworząc złożone zachowania z prostych bloków:
1// Hook 1: Prosty hook stanu z walidacją
2function useValidatedInput(initialValue, validator) {
3 const [value, setValue] = useState(initialValue);
4 const [error, setError] = useState(null);
5
6 const handleChange = (newValue) => {
7 setValue(newValue);
8 const validationError = validator(newValue);
9 setError(validationError);
10 };
11
12 return { value, error, onChange: handleChange, isValid: !error };
13}
14
15// Hook 2: Kompozycja - formularz z wieloma polami
16function useSpaceshipForm() {
17 const name = useValidatedInput('', (v) =>
18 v.length < 3 ? 'Min. 3 znaki' : null
19 );
20 const fuel = useValidatedInput(100, (v) =>
21 v < 0 || v > 100 ? 'Zakres 0-100' : null
22 );
23
24 const isFormValid = name.isValid && fuel.isValid;
25
26 const reset = () => {
27 name.onChange('');
28 fuel.onChange(100);
29 };
30
31 return { name, fuel, isFormValid, reset };
32}
33
34// Użycie
35function ShipRegistration() {
36 const form = useSpaceshipForm();
37
38 return (
39 <form>
40 <input
41 value={form.name.value}
42 onChange={e => form.name.onChange(e.target.value)}
43 />
44 {form.name.error && <span>{form.name.error}</span>}
45
46 <input
47 type="number"
48 value={form.fuel.value}
49 onChange={e => form.fuel.onChange(Number(e.target.value))}
50 />
51 {form.fuel.error && <span>{form.fuel.error}</span>}
52
53 <button disabled={!form.isFormValid}>Zarejestruj statek</button>
54 </form>
55 );
56}useSpaceshipForm używa useValidatedInput dwukrotnie - każde wywołanie tworzy niezależną instancję ze swoim stanem.
Wzorzec: useToggle, useCounter, useForm
Popularne wzorce custom hooków, które warto znać:
| Hook | Zastosowanie | Zwraca |
|---|---|---|
useToggle | Przełącznik on/off | { value, toggle, setTrue, setFalse } |
useCounter | Licznik z min/max | { count, increment, decrement, reset } |
useFetch | Pobieranie danych | { data, loading, error, refetch } |
useDebounce | Opóźnianie wartości | Opóźniona wartość |
usePrevious | Poprzednia wartość | Poprzednia wartość |
useArray | Operacje na tablicach | { array, push, remove, update, clear } |
useLocalStorage | Persystentny stan | [value, setValue] |
Podsumowanie
Custom Hooks to fundament dobrze zorganizowanej aplikacji React:
- Reguły hooków - zawsze na najwyższym poziomie, tylko w komponentach i hookach
- Prefiks
use- obowiązkowy, pozwala Reactowi weryfikować reguły - Kompozycja - hooki mogą wywoływać inne hooki, budując złożone zachowania
- Niezależne instancje - każde wywołanie hooka ma własny, izolowany stan
- Reużywalność - jedna logika, wiele komponentów
Jak modułowe systemy kosmicznego statku - każdy hook to samodzielny moduł, który można zainstalować, skonfigurować i połączyć z innymi modułami w dowolnej konfiguracji.
Kod do tej lekcji: App.jsx
1import React, { useState, useEffect, useRef } from 'react';
2
3// === useDebounce ===
4function useDebounce(value, delay) {
5 const [debouncedValue, setDebouncedValue] = useState(value);
6
7 useEffect(() => {
8 const timer = setTimeout(() => setDebouncedValue(value), delay);
9 return () => clearTimeout(timer);
10 }, [value, delay]);
11
12 return debouncedValue;
13}
14
15// === usePrevious ===
16function usePrevious(value) {
17 const ref = useRef();
18 useEffect(() => { ref.current = value; }, [value]);
19 return ref.current;
20}
21
22// === useToggle ===
23function useToggle(initial = false) {
24 const [value, setValue] = useState(initial);
25 const toggle = () => setValue(v => !v);
26 const setTrue = () => setValue(true);
27 const setFalse = () => setValue(false);
28 return { value, toggle, setTrue, setFalse };
29}
30
31// === useArray ===
32function useArray(initialArray = []) {
33 const [array, setArray] = useState(initialArray);
34 const push = (el) => setArray(a => [...a, el]);
35 const removeByIndex = (i) => setArray(a => a.filter((_, idx) => idx !== i));
36 const clear = () => setArray([]);
37 return { array, push, removeByIndex, clear, set: setArray };
38}
39
40// === useLocalStorage ===
41function useLocalStorage(key, initialValue) {
42 const [storedValue, setStoredValue] = useState(() => {
43 try {
44 const item = window.localStorage.getItem(key);
45 return item ? JSON.parse(item) : initialValue;
46 } catch { return initialValue; }
47 });
48
49 useEffect(() => {
50 try { window.localStorage.setItem(key, JSON.stringify(storedValue)); }
51 catch (e) { console.error(e); }
52 }, [key, storedValue]);
53
54 return [storedValue, setStoredValue];
55}
56
57// --- Demo Components ---
58
59function DebounceDemo() {
60 const [text, setText] = useState('');
61 const debouncedText = useDebounce(text, 600);
62 const [results, setResults] = useState([]);
63
64 useEffect(() => {
65 if (debouncedText) {
66 const planets = ['Merkury','Wenus','Ziemia','Mars','Jowisz','Saturn','Uran','Neptun'];
67 setResults(planets.filter(p =>
68 p.toLowerCase().includes(debouncedText.toLowerCase())
69 ));
70 } else {
71 setResults([]);
72 }
73 }, [debouncedText]);
74
75 return (
76 <div className="demo-card">
77 <h3>useDebounce</h3>
78 <input value={text} onChange={e => setText(e.target.value)} placeholder="Szukaj planet..." />
79 <p className="hint">Debounced: "{debouncedText}"</p>
80 {results.length > 0 && (
81 <ul className="results">{results.map(r => <li key={r}>{r}</li>)}</ul>
82 )}
83 </div>
84 );
85}
86
87function PreviousDemo() {
88 const [speed, setSpeed] = useState(0);
89 const prevSpeed = usePrevious(speed);
90
91 const trend = speed > (prevSpeed || 0) ? 'Przyspieszanie' :
92 speed < (prevSpeed || 0) ? 'Hamowanie' : 'Stala predkosc';
93
94 return (
95 <div className="demo-card">
96 <h3>usePrevious</h3>
97 <p className="speed-display">{speed} km/s</p>
98 <p className="hint">Poprzednio: {prevSpeed ?? 0} km/s | {trend}</p>
99 <div className="btn-row">
100 <button onClick={() => setSpeed(s => s + 10)}>+10</button>
101 <button onClick={() => setSpeed(s => Math.max(0, s - 10))}>-10</button>
102 <button onClick={() => setSpeed(0)}>Reset</button>
103 </div>
104 </div>
105 );
106}
107
108function ToggleDemo() {
109 const shields = useToggle(false);
110 const engines = useToggle(true);
111
112 return (
113 <div className="demo-card">
114 <h3>useToggle</h3>
115 <div className="toggle-item">
116 <span>Tarcze: <b style={{color: shields.value ? '#2ecc71' : '#e74c3c'}}>
117 {shields.value ? 'AKTYWNE' : 'WYLACZONE'}
118 </b></span>
119 <button onClick={shields.toggle}>Przelacz</button>
120 </div>
121 <div className="toggle-item">
122 <span>Silniki: <b style={{color: engines.value ? '#2ecc71' : '#e74c3c'}}>
123 {engines.value ? 'AKTYWNE' : 'WYLACZONE'}
124 </b></span>
125 <button onClick={engines.toggle}>Przelacz</button>
126 </div>
127 </div>
128 );
129}
130
131function ArrayDemo() {
132 const crew = useArray(['Kapitan Nova', 'Nawigator Rex']);
133 const [name, setName] = useState('');
134
135 const addMember = () => {
136 if (name.trim()) { crew.push(name.trim()); setName(''); }
137 };
138
139 return (
140 <div className="demo-card">
141 <h3>useArray</h3>
142 <div className="crew-list">
143 {crew.array.map((member, i) => (
144 <div key={i} className="crew-member">
145 <span>{member}</span>
146 <button onClick={() => crew.removeByIndex(i)} className="btn-small">X</button>
147 </div>
148 ))}
149 </div>
150 <div className="add-row">
151 <input value={name} onChange={e => setName(e.target.value)}
152 placeholder="Imie czlonka zalogi..." onKeyDown={e => e.key === 'Enter' && addMember()} />
153 <button onClick={addMember}>Dodaj</button>
154 </div>
155 <button onClick={crew.clear} className="btn-danger">Zwolnij zaloge</button>
156 </div>
157 );
158}
159
160function StorageDemo() {
161 const [theme, setTheme] = useLocalStorage('space-theme', 'dark');
162 const [volume, setVolume] = useLocalStorage('space-volume', 50);
163
164 return (
165 <div className="demo-card">
166 <h3>useLocalStorage</h3>
167 <div className="setting">
168 <label>Motyw:</label>
169 <select value={theme} onChange={e => setTheme(e.target.value)}>
170 <option value="dark">Ciemny</option>
171 <option value="light">Jasny</option>
172 <option value="cosmic">Kosmiczny</option>
173 </select>
174 </div>
175 <div className="setting">
176 <label>Glosnosc: {volume}%</label>
177 <input type="range" min="0" max="100" value={volume}
178 onChange={e => setVolume(Number(e.target.value))} />
179 </div>
180 <p className="hint">Wartosci zapisywane w localStorage</p>
181 </div>
182 );
183}
184
185export default function App() {
186 return (
187 <div className="app">
188 <h1>Custom Hooks - Moduly statku kosmicznego</h1>
189 <div className="grid">
190 <DebounceDemo />
191 <PreviousDemo />
192 <ToggleDemo />
193 <ArrayDemo />
194 <StorageDemo />
195 </div>
196 </div>
197 );
198}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Dlaczego NIE można wywoływać hooków wewnątrz instrukcji warunkowych (if/else)?
2. Czym custom hook różni się od zwykłej funkcji JavaScript?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Stwórz custom hook useDebounce(value, delay) zwracający opóźnioną wartość oraz useToggle(initialValue) zwracający { value, toggle, setTrue, setFalse }. Oba hooki powinny działać z komponentami testowymi na stronie.
- Układanie w pionie
Uporządkuj etapy tworzenia custom hooka od początku do końca:
- Edytor kodu
Stwórz custom hook useArray(initialArray) zwracający { array, set, push, removeByIndex, update, clear, filter } oraz usePrevious(value) zwracający poprzednią wartość. Hooki powinny działać z komponentem zarządzania załogą na stronie.
- Klikanie w kolejności
Ułóż składnię custom hooka useDebounce w poprawnej kolejności:
- Edytor kodu
Zbuduj kompleksowy Saturn Ring Observatory Control Panel zawierający: useEffect dla monitoring różnych faz orbitalnych pierścieni, useLayoutEffect dla synchronizacji pomiarów w czasie rzeczywistym, cleanup functions dla proper unmounting sensors, dependency arrays dla precyzyjnej kontroli re-runs, multiple effects dla niezależnych systemów monitoringu (RingA, RingB, RingC monitoring), effect optimization patterns, async effects dla data fetching z sond kosmicznych, oraz conditional effects reagujące na różne stany obserwatorium. Stwórz system monitoringu godny najbardziej zaawansowanego obserwatorium pierścieni Saturna!