Kurs JavaScript i React · Moduł 5: useEffect i własne hooki
Własne hooki (Custom Hooks) do efektów
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
- Nazwa zaczyna się od
use- to nie jest opcjonalne. React wymaga tego prefiksu, aby móc sprawdzać reguły hooków - Mogą wywoływać inne hooki - useState, useEffect, useRef, a nawet inne custom hooki
- Każde wywołanie ma własny stan - dwa komponenty używające tego samego hooka mają niezależne kopie stanu
- 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:
- Eliminują duplikację - powtarzalna logika efektów w jednym miejscu
- Poprawiają czytelność - komponenty stają się krótsze i bardziej deklaratywne
- Ułatwiają testowanie - logikę hooka można testować niezależnie od komponentu
- 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. Które z poniższych NIE jest typowym zastosowaniem useEffect?
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