Kurs JavaScript i React · Moduł 5: useEffect i własne hooki

Tworzenie własnych hooków (Custom Hooks) - zaawansowane wzorce

6 min czytania
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ć:

HookZastosowanieZwraca
useTogglePrzełącznik on/off{ value, toggle, setTrue, setFalse }
useCounterLicznik z min/max{ count, increment, decrement, reset }
useFetchPobieranie danych{ data, loading, error, refetch }
useDebounceOpóźnianie wartościOpóźniona wartość
usePreviousPoprzednia wartośćPoprzednia wartość
useArrayOperacje na tablicach{ array, push, remove, update, clear }
useLocalStoragePersystentny stan[value, setValue]

Podsumowanie

Custom Hooks to fundament dobrze zorganizowanej aplikacji React:

  1. Reguły hooków - zawsze na najwyższym poziomie, tylko w komponentach i hookach
  2. Prefiks use - obowiązkowy, pozwala Reactowi weryfikować reguły
  3. Kompozycja - hooki mogą wywoływać inne hooki, budując złożone zachowania
  4. Niezależne instancje - każde wywołanie hooka ma własny, izolowany stan
  5. 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. 1. Dlaczego NIE można wywoływać hooków wewnątrz instrukcji warunkowych (if/else)?

  2. 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!

Przydatne artykuły