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

Własne hooki (Custom Hooks) do efektów

5 min czytania
W tej lekcji6

W kosmicznej podróży przez React, wielokrotne pisanie tych samych wzorców useEffect w różnych komponentach to jak budowanie od zera identycznych systemów nawigacyjnych dla każdego statku we flocie. Custom Hooks pozwalają wyekstrahować powtarzalną logikę efektów do jednej, wielokrotnie używanej funkcji - jak stworzenie modułu napędowego, który można zainstalować w dowolnym statku.

Czym są Custom Hooks?

Custom Hook to zwykła funkcja JavaScript, której nazwa zaczyna się od use i która może wywoływać inne hooki (useState, useEffect, useRef itp.). Nie jest to żadna specjalna składnia Reacta - jedyną konwencją jest prefiks use.

1// Custom Hook - wyekstrahowana logika
2function useWindowSize() {
3  const [size, setSize] = useState({
4    width: window.innerWidth,
5    height: window.innerHeight
6  });
7
8  useEffect(() => {
9    const handleResize = () => {
10      setSize({ width: window.innerWidth, height: window.innerHeight });
11    };
12
13    window.addEventListener('resize', handleResize);
14    return () => window.removeEventListener('resize', handleResize);
15  }, []);
16
17  return size;
18}
19
20// Użycie w komponencie - czysto i prosto
21function SpaceshipCockpit() {
22  const { width, height } = useWindowSize();
23
24  return (
25    <p>
26      Rozmiar ekranu kokpitu: {width} x {height}
27    </p>
28  );
29}

Zauważ, że useWindowSize zawiera wewnątrz useState i useEffect z cleanup - cała logika jest zamknięta w jednym hooku, a komponent dostaje gotowy wynik.

useFetch - pobieranie danych

Jednym z najczęstszych custom hooków jest hook do pobierania danych. Zamiast powtarzać wzorzec loading/error/data w każdym komponencie, zamykamy go w useFetch:

1function useFetch(url) {
2  const [data, setData] = useState(null);
3  const [isLoading, setIsLoading] = useState(true);
4  const [error, setError] = useState(null);
5
6  useEffect(() => {
7    const controller = new AbortController();
8    setIsLoading(true);
9    setError(null);
10
11    fetch(url, { signal: controller.signal })
12      .then(response => {
13        if (!response.ok) throw new Error('Błąd sieci');
14        return response.json();
15      })
16      .then(data => {
17        setData(data);
18        setIsLoading(false);
19      })
20      .catch(err => {
21        if (err.name !== 'AbortError') {
22          setError(err.message);
23          setIsLoading(false);
24        }
25      });
26
27    // Cleanup - anuluj request przy odmontowaniu lub zmianie URL
28    return () => controller.abort();
29  }, [url]);
30
31  return { data, isLoading, error };
32}

Teraz każdy komponent potrzebujący danych z API może użyć jednej linii:

1function CrewList() {
2  const { data: crew, isLoading, error } = useFetch('/api/crew');
3
4  if (isLoading) return <p>Ładowanie załogi...</p>;
5  if (error) return <p>Błąd: {error}</p>;
6
7  return (
8    <ul>
9      {crew.map(member => (
10        <li key={member.id}>{member.name}</li>
11      ))}
12    </ul>
13  );
14}
15
16function MissionLog() {
17  const { data: missions, isLoading } = useFetch('/api/missions');
18
19  if (isLoading) return <p>Ładowanie misji...</p>;
20
21  return missions.map(m => <div key={m.id}>{m.name}</div>);
22}

Dwa komponenty, ten sam wzorzec pobierania danych - zero duplikacji kodu.

useInterval - bezpieczny timer

Timer z setInterval wymaga cleanup i poprawnej obsługi zależności. Custom hook zamyka tę logikę:

1function useInterval(callback, delay) {
2  const savedCallback = useRef();
3
4  // Zapamiętaj najnowszą wersję callback
5  useEffect(() => {
6    savedCallback.current = callback;
7  }, [callback]);
8
9  // Ustaw timer
10  useEffect(() => {
11    if (delay === null) return; // null = pauzuj timer
12
13    const tick = () => savedCallback.current();
14    const id = setInterval(tick, delay);
15    return () => clearInterval(id);
16  }, [delay]);
17}
18
19// Użycie
20function MissionClock() {
21  const [seconds, setSeconds] = useState(0);
22  const [isRunning, setIsRunning] = useState(false);
23
24  useInterval(() => {
25    setSeconds(prev => prev + 1);
26  }, isRunning ? 1000 : null); // null = pauzuj
27
28  return (
29    <div>
30      <p>Czas misji: {seconds}s</p>
31      <button onClick={() => setIsRunning(!isRunning)}>
32        {isRunning ? 'Pauza' : 'Start'}
33      </button>
34    </div>
35  );
36}

Dzięki useRef dla callback, timer zawsze wywołuje najnowszą wersję funkcji bez konieczności restartowania interwału.

useLocalStorage - trwały stan

Hook synchronizujący stan z localStorage:

1function useLocalStorage(key, initialValue) {
2  const [storedValue, setStoredValue] = useState(() => {
3    try {
4      const item = window.localStorage.getItem(key);
5      return item ? JSON.parse(item) : initialValue;
6    } catch (error) {
7      return initialValue;
8    }
9  });
10
11  useEffect(() => {
12    try {
13      window.localStorage.setItem(key, JSON.stringify(storedValue));
14    } catch (error) {
15      console.error('Błąd zapisu do localStorage:', error);
16    }
17  }, [key, storedValue]);
18
19  return [storedValue, setStoredValue];
20}
21
22// Użycie - działa jak useState, ale persystuje dane
23function ShipSettings() {
24  const [theme, setTheme] = useLocalStorage('theme', 'dark');
25  const [volume, setVolume] = useLocalStorage('volume', 50);
26
27  return (
28    <div>
29      <select value={theme} onChange={e => setTheme(e.target.value)}>
30        <option value="dark">Ciemny</option>
31        <option value="light">Jasny</option>
32      </select>
33      <input
34        type="range"
35        value={volume}
36        onChange={e => setVolume(Number(e.target.value))}
37      />
38    </div>
39  );
40}

Po odświeżeniu strony ustawienia są zachowane - localStorage pamięta wartości między sesjami.

Zasady tworzenia Custom Hooks

  1. Nazwa zaczyna się od use - to nie jest opcjonalne. React wymaga tego prefiksu, aby móc sprawdzać reguły hooków
  2. Mogą wywoływać inne hooki - useState, useEffect, useRef, a nawet inne custom hooki
  3. Każde wywołanie ma własny stan - dwa komponenty używające tego samego hooka mają niezależne kopie stanu
  4. Zwracają to, czego potrzebuje komponent - mogą zwracać wartości, tablice, obiekty, funkcje
1// Custom Hook z wieloma wartościami zwracanymi
2function useToggle(initialValue = false) {
3  const [value, setValue] = useState(initialValue);
4
5  const toggle = () => setValue(prev => !prev);
6  const setTrue = () => setValue(true);
7  const setFalse = () => setValue(false);
8
9  return { value, toggle, setTrue, setFalse };
10}
11
12// Użycie
13function ShieldControl() {
14  const shields = useToggle(false);
15
16  return (
17    <div>
18      <p>Tarcze: {shields.value ? 'AKTYWNE' : 'WYŁĄCZONE'}</p>
19      <button onClick={shields.toggle}>Przełącz</button>
20      <button onClick={shields.setTrue}>Aktywuj</button>
21      <button onClick={shields.setFalse}>Dezaktywuj</button>
22    </div>
23  );
24}

Podsumowanie

Custom Hooks to jedno z najpotężniejszych narzędzi w React do organizacji kodu:

  1. Eliminują duplikację - powtarzalna logika efektów w jednym miejscu
  2. Poprawiają czytelność - komponenty stają się krótsze i bardziej deklaratywne
  3. Ułatwiają testowanie - logikę hooka można testować niezależnie od komponentu
  4. Są kompozycyjne - custom hooki mogą wywoływać inne custom hooki

Jak modułowe systemy statku kosmicznego - system nawigacyjny, system komunikacyjny, system obronny - każdy jest osobnym modułem (hookiem), który można zainstalować w dowolnym statku (komponencie).

Kod do tej lekcji: App.jsx
1import React, { useState, useEffect, useRef } from 'react';
2
3// Custom Hook: useToggle
4function useToggle(initialValue = false) {
5  const [value, setValue] = useState(initialValue);
6  const toggle = () => setValue(prev => !prev);
7  const setTrue = () => setValue(true);
8  const setFalse = () => setValue(false);
9  return { value, toggle, setTrue, setFalse };
10}
11
12// Custom Hook: useInterval
13function useInterval(callback, delay) {
14  const savedCallback = useRef();
15
16  useEffect(() => {
17    savedCallback.current = callback;
18  }, [callback]);
19
20  useEffect(() => {
21    if (delay === null) return;
22    const tick = () => savedCallback.current();
23    const id = setInterval(tick, delay);
24    return () => clearInterval(id);
25  }, [delay]);
26}
27
28// Custom Hook: useLocalStorage
29function useLocalStorage(key, initialValue) {
30  const [storedValue, setStoredValue] = useState(() => {
31    try {
32      const item = window.localStorage.getItem(key);
33      return item ? JSON.parse(item) : initialValue;
34    } catch {
35      return initialValue;
36    }
37  });
38
39  useEffect(() => {
40    try {
41      window.localStorage.setItem(key, JSON.stringify(storedValue));
42    } catch (error) {
43      console.error('Blad zapisu:', error);
44    }
45  }, [key, storedValue]);
46
47  return [storedValue, setStoredValue];
48}
49
50// Demo 1: useToggle
51function ShieldControls() {
52  const shields = useToggle(false);
53  const weapons = useToggle(false);
54  const engines = useToggle(true);
55
56  return (
57    <div className="demo-card">
58      <h2>useToggle</h2>
59      <div className="system-row">
60        <span>Tarcze</span>
61        <span className={shields.value ? 'on' : 'off'}>{shields.value ? 'ON' : 'OFF'}</span>
62        <button onClick={shields.toggle}>Przelacz</button>
63      </div>
64      <div className="system-row">
65        <span>Uzbrojenie</span>
66        <span className={weapons.value ? 'on' : 'off'}>{weapons.value ? 'ON' : 'OFF'}</span>
67        <button onClick={weapons.toggle}>Przelacz</button>
68      </div>
69      <div className="system-row">
70        <span>Silniki</span>
71        <span className={engines.value ? 'on' : 'off'}>{engines.value ? 'ON' : 'OFF'}</span>
72        <button onClick={engines.toggle}>Przelacz</button>
73      </div>
74    </div>
75  );
76}
77
78// Demo 2: useInterval
79function MissionClock() {
80  const [seconds, setSeconds] = useState(0);
81  const isRunning = useToggle(false);
82
83  useInterval(() => {
84    setSeconds(prev => prev + 1);
85  }, isRunning.value ? 1000 : null);
86
87  const mins = String(Math.floor(seconds / 60)).padStart(2, '0');
88  const secs = String(seconds % 60).padStart(2, '0');
89
90  return (
91    <div className="demo-card">
92      <h2>useInterval</h2>
93      <div className="clock">{mins}:{secs}</div>
94      <div className="clock-controls">
95        <button onClick={isRunning.toggle}>
96          {isRunning.value ? 'Pauza' : 'Start'}
97        </button>
98        <button onClick={() => { isRunning.setFalse(); setSeconds(0); }}>
99          Reset
100        </button>
101      </div>
102    </div>
103  );
104}
105
106// Demo 3: useLocalStorage
107function ShipSettings() {
108  const [pilotName, setPilotName] = useLocalStorage('pilotName', '');
109  const [theme, setTheme] = useLocalStorage('theme', 'dark');
110  const [volume, setVolume] = useLocalStorage('volume', 50);
111
112  return (
113    <div className="demo-card">
114      <h2>useLocalStorage</h2>
115      <p className="hint">Dane przetrwaja odswiezenie strony!</p>
116      <div className="setting">
117        <label>Pilot:</label>
118        <input
119          value={pilotName}
120          onChange={e => setPilotName(e.target.value)}
121          placeholder="Wpisz imie..."
122        />
123      </div>
124      <div className="setting">
125        <label>Motyw:</label>
126        <select value={theme} onChange={e => setTheme(e.target.value)}>
127          <option value="dark">Ciemny</option>
128          <option value="light">Jasny</option>
129          <option value="blue">Kosmiczny</option>
130        </select>
131      </div>
132      <div className="setting">
133        <label>Glosnosc: {volume}%</label>
134        <input
135          type="range"
136          min="0"
137          max="100"
138          value={volume}
139          onChange={e => setVolume(Number(e.target.value))}
140        />
141      </div>
142    </div>
143  );
144}
145
146function App() {
147  return (
148    <div className="app">
149      <h1>Custom Hooks - Modulowe systemy statku</h1>
150      <div className="demos">
151        <ShieldControls />
152        <MissionClock />
153        <ShipSettings />
154      </div>
155    </div>
156  );
157}
158
159export default App;

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. Które z poniższych NIE jest typowym zastosowaniem useEffect?

  2. 2. Który hook React zastępuje metodę componentDidMount z komponentów klasowych?

Zadania praktyczne w grze

  • Edytor kodu

    Własny hook media query

  • Edytor kodu

    Monitorowanie stanu baterii

Przydatne artykuły