Kurs JavaScript i React · Moduł 4: Context API i useReducer
useReducer - zaawansowane zarządzanie stanem
W tej lekcji8
W centrum dowodzenia statku kosmicznego, prosty panel z jednym przyciskiem to za mało. Gdy systemy pokładowe stają się coraz bardziej złożone - tarcze, silniki, systemy nawigacji, alarmy - potrzebujemy bardziej uporządkowanego sposobu zarządzania stanem. Hook useReducer to jak centralny komputer pokładowy, który przyjmuje rozkazy (akcje) i na ich podstawie aktualizuje stan wszystkich systemów według ściśle określonych reguł.
Czym jest useReducer?
useReducer to alternatywa dla useState, zaprojektowana do obsługi bardziej złożonej logiki stanu. Zamiast bezpośrednio ustawiać nową wartość, wysyłasz akcję (rozkaz), a specjalna funkcja zwana reducerem decyduje, jak stan powinien się zmienić.
Podstawowa składnia
1const [state, dispatch] = useReducer(reducer, initialState);Gdzie:
state- aktualny stan (dane systemów statku)dispatch- funkcja do wysyłania akcji (wydawania rozkazów)reducer- funkcja, która na podstawie bieżącego stanu i akcji zwraca nowy staninitialState- początkowa wartość stanu
Analogia kosmiczna: Pomyśl o dispatch jako o panelu sterowania, na którym wciskasz przyciski (akcje). Komputer pokładowy (reducer) odbiera rozkaz i na podstawie aktualnego stanu statku oblicza nowy stan. Nigdy nie modyfikujesz systemów ręcznie - zawsze przez komputer!
Funkcja reducer
Reducer to czysta funkcja, która przyjmuje dwa argumenty: aktualny stan i akcję. Na ich podstawie zwraca nowy obiekt stanu. Najczęściej używamy instrukcji switch do obsługi różnych typów akcji:
1function spaceshipReducer(state, action) {
2 switch (action.type) {
3 case 'ACTIVATE_SHIELDS':
4 return { ...state, shields: 100, status: 'defended' };
5
6 case 'FIRE_ENGINES':
7 return { ...state, speed: state.speed + action.payload, fuel: state.fuel - 10 };
8
9 case 'EMERGENCY_STOP':
10 return { ...state, speed: 0, status: 'stopped' };
11
12 default:
13 return state;
14 }
15}Kluczowe zasady reducera:
- Zawsze zwracaj nowy obiekt stanu (nie mutuj poprzedniego)
- Obsłuż
default- zwracaj niezmieniony stan dla nieznanych akcji - Reducer powinien być czystą funkcją - bez side effects, bez losowości
Dispatch i obiekty akcji
Akcja to zwykły obiekt JavaScript z polem type opisującym rodzaj operacji. Opcjonalnie możesz dodać pole payload z dodatkowymi danymi:
1// Prosta akcja - tylko typ
2dispatch({ type: 'ACTIVATE_SHIELDS' });
3
4// Akcja z danymi (payload)
5dispatch({ type: 'FIRE_ENGINES', payload: 50 });
6
7// Akcja ze złożonym payloadem
8dispatch({
9 type: 'ADD_CREW_MEMBER',
10 payload: { name: 'Kapitan Nova', role: 'pilot' }
11});Praktyczny przykład - panel sterowania statkiem
Zobaczmy, jak useReducer sprawdza się w zarządzaniu złożonym stanem panelu kontrolnego:
1import { useReducer } from 'react';
2
3const initialShipState = {
4 fuel: 100,
5 shields: 0,
6 speed: 0,
7 status: 'docked',
8 log: []
9};
10
11function shipReducer(state, action) {
12 switch (action.type) {
13 case 'LAUNCH':
14 if (state.fuel < 20) return state;
15 return {
16 ...state,
17 status: 'flying',
18 fuel: state.fuel - 20,
19 speed: 50,
20 log: [...state.log, 'Statek wystartował']
21 };
22
23 case 'BOOST':
24 if (state.status !== 'flying' || state.fuel < 10) return state;
25 return {
26 ...state,
27 speed: state.speed + action.payload,
28 fuel: state.fuel - 10,
29 log: [...state.log, `Przyspieszono o ${action.payload} km/s`]
30 };
31
32 case 'TOGGLE_SHIELDS':
33 return {
34 ...state,
35 shields: state.shields > 0 ? 0 : 100,
36 log: [...state.log, state.shields > 0 ? 'Tarcze wyłączone' : 'Tarcze aktywowane']
37 };
38
39 case 'DOCK':
40 return {
41 ...state,
42 status: 'docked',
43 speed: 0,
44 log: [...state.log, 'Statek zadokował']
45 };
46
47 case 'REFUEL':
48 return {
49 ...state,
50 fuel: 100,
51 log: [...state.log, 'Zatankowano paliwo']
52 };
53
54 default:
55 return state;
56 }
57}
58
59function ShipControlPanel() {
60 const [ship, dispatch] = useReducer(shipReducer, initialShipState);
61
62 return (
63 <div className="control-panel">
64 <h2>Panel sterowania</h2>
65 <p>Status: {ship.status}</p>
66 <p>Paliwo: {ship.fuel}%</p>
67 <p>Tarcze: {ship.shields}%</p>
68 <p>Prędkość: {ship.speed} km/s</p>
69
70 <button onClick={() => dispatch({ type: 'LAUNCH' })}
71 disabled={ship.status === 'flying'}>
72 Start
73 </button>
74 <button onClick={() => dispatch({ type: 'BOOST', payload: 25 })}
75 disabled={ship.status !== 'flying'}>
76 Przyspiesz (+25)
77 </button>
78 <button onClick={() => dispatch({ type: 'TOGGLE_SHIELDS' })}>
79 {ship.shields > 0 ? 'Wyłącz tarcze' : 'Włącz tarcze'}
80 </button>
81 <button onClick={() => dispatch({ type: 'DOCK' })}
82 disabled={ship.status === 'docked'}>
83 Dokuj
84 </button>
85 <button onClick={() => dispatch({ type: 'REFUEL' })}
86 disabled={ship.status === 'flying'}>
87 Zatankuj
88 </button>
89
90 <div className="ship-log">
91 <h3>Dziennik pokładowy:</h3>
92 <ul>
93 {ship.log.map((entry, i) => (
94 <li key={i}>{entry}</li>
95 ))}
96 </ul>
97 </div>
98 </div>
99 );
100}Zwróć uwagę, jak reducer centralizuje całą logikę aktualizacji stanu. Komponent nie musi wiedzieć, jak dokładnie zmienia się stan - wystarczy, że wyśle odpowiednią akcję. To ogromna zaleta przy rozbudowanych interfejsach.
Kiedy useReducer zamiast useState?
Oba hooki służą do zarządzania stanem, ale mają różne zastosowania:
Wybierz useState gdy:
- Stan jest prosty (liczba, string, boolean)
- Logika aktualizacji jest prosta (bezpośrednie przypisanie)
- Masz kilka niezależnych wartości
Wybierz useReducer gdy:
- Stan jest złożonym obiektem z wieloma polami
- Aktualizacja jednego pola wymaga zmiany innych pól
- Masz wiele powiązanych akcji (np. start, stop, reset, boost)
- Logika aktualizacji zawiera warunki (np. nie startuj bez paliwa)
- Chcesz łatwo testować logikę stanu oddzielnie od komponentu
1// useState - wystarczy dla prostych przypadków
2const [isOpen, setIsOpen] = useState(false);
3const [count, setCount] = useState(0);
4
5// useReducer - lepszy dla złożonego stanu
6const [shipState, dispatch] = useReducer(shipReducer, {
7 fuel: 100,
8 shields: 0,
9 speed: 0,
10 status: 'docked',
11 crew: [],
12 alerts: [],
13 systems: { engine: 'online', navigation: 'online', weapons: 'offline' }
14});Wzorzec switch-case w reducerze
Instrukcja switch to standardowy wzorzec w reducerach. Oto kilka dobrych praktyk:
1// Definiuj stałe dla typów akcji - unikasz literówek
2const ACTIONS = {
3 LAUNCH: 'LAUNCH',
4 DOCK: 'DOCK',
5 BOOST: 'BOOST',
6 TAKE_DAMAGE: 'TAKE_DAMAGE',
7 REPAIR: 'REPAIR'
8};
9
10function shipReducer(state, action) {
11 switch (action.type) {
12 case ACTIONS.LAUNCH:
13 return { ...state, status: 'flying', speed: 50 };
14
15 case ACTIONS.DOCK:
16 return { ...state, status: 'docked', speed: 0 };
17
18 case ACTIONS.BOOST:
19 return {
20 ...state,
21 speed: Math.min(state.speed + action.payload, 300)
22 };
23
24 case ACTIONS.TAKE_DAMAGE: {
25 const newShields = Math.max(0, state.shields - action.payload);
26 return {
27 ...state,
28 shields: newShields,
29 status: newShields === 0 ? 'critical' : state.status
30 };
31 }
32
33 case ACTIONS.REPAIR:
34 return { ...state, shields: 100, status: 'docked' };
35
36 default:
37 throw new Error(`Nieznana akcja: ${action.type}`);
38 }
39}
40
41// Użycie ze stałymi
42dispatch({ type: ACTIONS.BOOST, payload: 50 });
43dispatch({ type: ACTIONS.TAKE_DAMAGE, payload: 30 });Zauważ, że w TAKE_DAMAGE użyliśmy bloku {} wewnątrz case - to pozwala na deklarację zmiennych lokalnych (newShields) bez konfliktów z innymi case'ami. Dodatkowo, zamiast default: return state, rzucamy błąd - to pomaga wyłapać literówki w nazwach akcji podczas programowania.
Kreatory akcji (action creators)
Dla wygody i czytelności możesz tworzyć funkcje, które generują obiekty akcji:
1// Kreatory akcji
2const launch = () => ({ type: 'LAUNCH' });
3const boost = (amount) => ({ type: 'BOOST', payload: amount });
4const takeDamage = (amount) => ({ type: 'TAKE_DAMAGE', payload: amount });
5const addCrewMember = (name, role) => ({
6 type: 'ADD_CREW_MEMBER',
7 payload: { name, role }
8});
9
10// Użycie - czytelniejsze niż ręczne tworzenie obiektów
11dispatch(launch());
12dispatch(boost(50));
13dispatch(takeDamage(25));
14dispatch(addCrewMember('Nova', 'pilot'));Kreatory akcji zmniejszają ryzyko literówek i centralizują logikę tworzenia akcji w jednym miejscu.
Podsumowanie
useReducer to potężne narzędzie do zarządzania złożonym stanem w React:
- Składnia:
const [state, dispatch] = useReducer(reducer, initialState) - Reducer: czysta funkcja
(state, action) => newStatez wzorcem switch-case - Dispatch: wysyłanie akcji
dispatch({ type: 'ACTION', payload: data }) - Kiedy używać: złożony stan, powiązane aktualizacje, logika biznesowa w stanie
- Dobre praktyki: stałe dla typów akcji, kreatory akcji, bloki
{}w case, rzucanie błędów w default
Tak jak centralny komputer pokładowy statku kosmicznego koordynuje pracę wszystkich systemów na podstawie rozkazów z mostka kapitańskiego, tak useReducer centralizuje logikę stanu, czyniąc ją przewidywalną i łatwą do zarządzania.
Kod do tej lekcji: App.jsx
1import React, { useState, useMemo, useCallback, memo } from 'react';
2
3// Komponent bez optymalizacji (dla porównania)
4function UnoptimizedItem({ name, onSelect }) {
5 console.log('Rendering UnoptimizedItem:', name);
6 return (
7 <div style={{
8 padding: '10px',
9 backgroundColor: '#ffebee',
10 marginBottom: '5px',
11 borderRadius: '4px'
12 }}>
13 {name}
14 <button onClick={() => onSelect(name)} style={{ marginLeft: '10px' }}>Select</button>
15 </div>
16 );
17}
18
19// Komponent zoptymalizowany z React.memo
20const OptimizedItem = memo(function OptimizedItem({ name, onSelect }) {
21 console.log('Rendering OptimizedItem:', name);
22 return (
23 <div style={{
24 padding: '10px',
25 backgroundColor: '#e8f5e9',
26 marginBottom: '5px',
27 borderRadius: '4px'
28 }}>
29 {name}
30 <button onClick={() => onSelect(name)} style={{ marginLeft: '10px' }}>Select</button>
31 </div>
32 );
33});
34
35// Ciężkie obliczenia (symulacja)
36function expensiveCalculation(numbers) {
37 console.log('Running expensive calculation...');
38 let result = 0;
39 for (let i = 0; i < 100000000; i++) {
40 result += numbers.reduce((a, b) => a + b, 0);
41 }
42 return numbers.reduce((a, b) => a + b, 0);
43}
44
45function PerformanceDemo() {
46 const [count, setCount] = useState(0);
47 const [numbers] = useState([1, 2, 3, 4, 5]);
48 const [selectedItem, setSelectedItem] = useState('');
49 const [darkMode, setDarkMode] = useState(false);
50
51 const items = ['Mars', 'Jowisz', 'Saturn', 'Uran', 'Neptun'];
52
53 // Bez useMemo - obliczenie przy każdym renderze
54 // const sum = expensiveCalculation(numbers);
55
56 // Z useMemo - obliczenie tylko gdy zmieni się 'numbers'
57 const sum = useMemo(() => {
58 console.log('Computing sum with useMemo...');
59 return numbers.reduce((a, b) => a + b, 0);
60 }, [numbers]);
61
62 // Bez useCallback - nowa funkcja przy każdym renderze
63 // const handleSelect = (name) => setSelectedItem(name);
64
65 // Z useCallback - stabilna referencja
66 const handleSelect = useCallback((name) => {
67 setSelectedItem(name);
68 }, []);
69
70 // Style zoptymalizowane z useMemo
71 const containerStyle = useMemo(() => ({
72 fontFamily: 'Arial, sans-serif',
73 padding: '20px',
74 maxWidth: '900px',
75 margin: '0 auto',
76 backgroundColor: darkMode ? '#1a1a2e' : '#f5f5f5',
77 color: darkMode ? 'white' : '#333',
78 minHeight: '100vh',
79 transition: 'all 0.3s ease'
80 }), [darkMode]);
81
82 return (
83 <div style={containerStyle}>
84 <h1 style={{ textAlign: 'center', color: '#667eea' }}>Performance Optimization</h1>
85
86 <div style={{
87 display: 'flex',
88 justifyContent: 'space-between',
89 alignItems: 'center',
90 marginBottom: '30px',
91 padding: '15px',
92 backgroundColor: darkMode ? '#252545' : '#fff',
93 borderRadius: '12px'
94 }}>
95 <div>
96 <button
97 onClick={() => setCount(c => c + 1)}
98 style={{
99 padding: '10px 20px',
100 backgroundColor: '#667eea',
101 color: 'white',
102 border: 'none',
103 borderRadius: '8px',
104 cursor: 'pointer',
105 marginRight: '10px'
106 }}
107 >
108 Count: {count}
109 </button>
110 <button
111 onClick={() => setDarkMode(!darkMode)}
112 style={{
113 padding: '10px 20px',
114 backgroundColor: darkMode ? '#fff' : '#333',
115 color: darkMode ? '#333' : 'white',
116 border: 'none',
117 borderRadius: '8px',
118 cursor: 'pointer'
119 }}
120 >
121 {darkMode ? 'Light' : 'Dark'}
122 </button>
123 </div>
124 <div>
125 Selected: <strong>{selectedItem || 'None'}</strong>
126 </div>
127 </div>
128
129 <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
130 {/* useMemo Demo */}
131 <section style={{
132 padding: '20px',
133 backgroundColor: darkMode ? '#252545' : '#fff',
134 borderRadius: '12px'
135 }}>
136 <h2 style={{ color: '#4caf50' }}>useMemo</h2>
137 <p>Suma tablicy: <strong>{sum}</strong></p>
138 <p style={{ fontSize: '14px', color: '#888' }}>
139 Obliczenia wykonywane tylko gdy zmieni się tablica 'numbers'.
140 Kliknij "Count" - suma nie będzie przeliczana!
141 </p>
142 <pre style={{
143 backgroundColor: darkMode ? '#1a1a2e' : '#f5f5f5',
144 padding: '10px',
145 borderRadius: '8px',
146 fontSize: '12px',
147 overflow: 'auto'
148 }}>
149{`const sum = useMemo(() => {
150 return numbers.reduce((a, b) => a + b, 0);
151}, [numbers]);`}
152 </pre>
153 </section>
154
155 {/* React.memo Demo */}
156 <section style={{
157 padding: '20px',
158 backgroundColor: darkMode ? '#252545' : '#fff',
159 borderRadius: '12px'
160 }}>
161 <h2 style={{ color: '#2196f3' }}>React.memo + useCallback</h2>
162 <p style={{ fontSize: '14px', color: '#888', marginBottom: '15px' }}>
163 Sprawdź konsolę! Zoptymalizowane elementy nie renderują się przy zmianie count.
164 </p>
165
166 <h4>Bez optymalizacji:</h4>
167 {items.slice(0, 2).map(item => (
168 <UnoptimizedItem
169 key={item}
170 name={item}
171 onSelect={(name) => setSelectedItem(name)}
172 />
173 ))}
174
175 <h4 style={{ marginTop: '20px' }}>Z React.memo + useCallback:</h4>
176 {items.slice(0, 2).map(item => (
177 <OptimizedItem
178 key={item}
179 name={item}
180 onSelect={handleSelect}
181 />
182 ))}
183 </section>
184 </div>
185
186 <section style={{
187 marginTop: '20px',
188 padding: '20px',
189 backgroundColor: darkMode ? '#252545' : '#fff',
190 borderRadius: '12px'
191 }}>
192 <h2 style={{ color: '#ff9800' }}>Kiedy optymalizować?</h2>
193 <ul style={{ lineHeight: '1.8' }}>
194 <li><strong>useMemo:</strong> Kosztowne obliczenia, tworzenie obiektów/tablic</li>
195 <li><strong>useCallback:</strong> Callbacki przekazywane do zoptymalizowanych dzieci</li>
196 <li><strong>React.memo:</strong> Komponenty, które często się nie zmieniają</li>
197 <li><strong>Uwaga:</strong> Nie optymalizuj przedwcześnie! Mierz najpierw.</li>
198 </ul>
199 </section>
200 </div>
201 );
202}
203
204export default PerformanceDemo;Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co zwraca hook useReducer?
Zadania praktyczne w grze
- Klikanie w kolejności
Zbuduj poprawną deklarację useReducer:
- Edytor kodu
Utwórz system stacji badawczej
- Układanie w poziomie
Ułóż poprawną strukturę obiektu akcji w useReducer:
- Układanie w pionie
Uporządkuj komponenty architektury aplikacji kosmicznej od najbardziej ogólnych do szczegółowych:
- Edytor kodu
Stwórz galaktyczny bank danych