Kurs JavaScript i React · Moduł 3: Stan i zdarzenia
Aktualizacja stanu - wzorce i pułapki
W tej lekcji8
Wyobraź sobie, że systemy nawigacyjne twojego statku kosmicznego działają na podstawie danych o stanie - prędkość, kurs, poziom paliwa. Każda aktualizacja tych parametrów musi być precyzyjna, bo nawet drobny błąd może wytrącić statek z orbity. W React aktualizacja stanu podlega podobnym zasadom - musisz znać wzorce bezpiecznej aktualizacji, aby twoja aplikacja działała przewidywalnie.
Aktualizacja funkcyjna vs bezpośrednia wartość
Gdy wywołujesz setState, masz dwie opcje. Pierwsza to przekazanie nowej wartości wprost:
1setCount(count + 1);Druga to aktualizacja funkcyjna - przekazanie funkcji, która otrzymuje poprzednią wartość stanu:
1setCount(prev => prev + 1);Dlaczego to ma znaczenie? React grupuje aktualizacje stanu (batching) i nie wykonuje ich natychmiast. Jeśli w jednym handlerze wywołasz setCount(count + 1) kilka razy, każde wywołanie zobaczy tę samą wartość count:
1function FuelGauge() {
2 const [fuel, setFuel] = useState(50);
3
4 const handleTripleBoost = () => {
5 // Wszystkie trzy widzą tę samą wartość fuel!
6 // Wynik: fuel wzrośnie tylko o 10, nie o 30
7 setFuel(fuel + 10);
8 setFuel(fuel + 10);
9 setFuel(fuel + 10);
10 };
11
12 const handleTripleBoostCorrect = () => {
13 // Każde wywołanie otrzymuje aktualną wartość
14 // Wynik: fuel wzrośnie o 30
15 setFuel(prev => prev + 10);
16 setFuel(prev => prev + 10);
17 setFuel(prev => prev + 10);
18 };
19
20 return (
21 <div>
22 <p>Paliwo: {fuel}%</p>
23 <button onClick={handleTripleBoost}>Boost (błędny)</button>
24 <button onClick={handleTripleBoostCorrect}>Boost (poprawny)</button>
25 </div>
26 );
27}Zasada: Zawsze używaj aktualizacji funkcyjnej (prev => ...), gdy nowa wartość zależy od poprzedniej.
Wiele zmiennych stanu vs jeden obiekt
Masz dwie strategie przechowywania stanu w komponencie. Pierwsza to oddzielne zmienne stanu:
1function ShipDashboard() {
2 const [speed, setSpeed] = useState(0);
3 const [altitude, setAltitude] = useState(0);
4 const [heading, setHeading] = useState(0);
5
6 // Każda zmienna aktualizowana niezależnie
7 const accelerate = () => setSpeed(prev => prev + 10);
8 const climb = () => setAltitude(prev => prev + 100);
9
10 return (
11 <div>
12 <p>Prędkość: {speed} km/s | Wysokość: {altitude} km | Kurs: {heading}°</p>
13 <button onClick={accelerate}>Przyspiesz</button>
14 <button onClick={climb}>Wznieś się</button>
15 </div>
16 );
17}Druga to stan w obiekcie - przydatna gdy dane są ze sobą powiązane:
1function ShipDashboard() {
2 const [navigation, setNavigation] = useState({
3 speed: 0,
4 altitude: 0,
5 heading: 0
6 });
7
8 // WAŻNE: zawsze kopiuj cały obiekt operatorem spread!
9 const accelerate = () => {
10 setNavigation(prev => ({ ...prev, speed: prev.speed + 10 }));
11 };
12
13 return (
14 <div>
15 <p>Prędkość: {navigation.speed} km/s</p>
16 <button onClick={accelerate}>Przyspiesz</button>
17 </div>
18 );
19}Kiedy co stosować?
- Oddzielne
useState- gdy wartości zmieniają się niezależnie od siebie - Obiekt w
useState- gdy wartości logicznie należą do jednej całości (np. formularz, pozycja x/y)
Najczęstsze błędy - mutacja stanu
W React stan jest niemutowalny (immutable). Nigdy nie modyfikuj stanu bezpośrednio - zawsze twórz nową kopię. To jak zasada bezpieczeństwa na statku kosmicznym: nie modyfikujesz dziennika pokładowego, tylko dopisujesz nowy wpis.
Błąd 1: push zamiast spread dla tablic
1function MissionLog() {
2 const [logs, setLogs] = useState(["Start misji"]);
3
4 const addLog = (entry) => {
5 // BŁĄD: push mutuje istniejącą tablicę!
6 // React nie wykryje zmiany i nie przerenderuje komponentu
7 logs.push(entry);
8 setLogs(logs);
9 };
10
11 const addLogCorrect = (entry) => {
12 // Spread tworzy nową tablicę
13 setLogs(prev => [...prev, entry]);
14 };
15
16 return (
17 <div>
18 <button onClick={() => addLogCorrect("Nowy wpis")}>
19 Dodaj wpis
20 </button>
21 {logs.map((log, i) => <p key={i}>{log}</p>)}
22 </div>
23 );
24}Błąd 2: bezpośrednia zmiana właściwości obiektu
1function CrewMember() {
2 const [member, setMember] = useState({
3 name: "Kapitan Nova",
4 rank: "Kapitan",
5 missions: 12
6 });
7
8 const promote = () => {
9 // BŁĄD: bezpośrednia mutacja obiektu
10 member.rank = "Admirał";
11 setMember(member);
12 };
13
14 const promoteCorrect = () => {
15 // Tworzenie nowego obiektu z operatorem spread
16 setMember(prev => ({ ...prev, rank: "Admirał" }));
17 };
18
19 return (
20 <div>
21 <p>{member.name} - {member.rank}</p>
22 <button onClick={promoteCorrect}>Awansuj</button>
23 </div>
24 );
25}Stale closures - pułapka zamknięć
Closure (domknięcie) to sytuacja, gdy funkcja "zapamiętuje" zmienne z momentu jej utworzenia. W React może to prowadzić do odczytu nieaktualnego stanu:
1function CountdownTimer() {
2 const [seconds, setSeconds] = useState(10);
3 const [running, setRunning] = useState(false);
4
5 const startCountdown = () => {
6 // seconds jest "zamrożone" z chwili kliknięcia!
7 // Każdy interwał widzi tę samą wartość
8 setInterval(() => {
9 setSeconds(seconds - 1); // zawsze 10 - 1 = 9
10 }, 1000);
11 };
12
13 const startCountdownCorrect = () => {
14 setRunning(true);
15 // Aktualizacja funkcyjna zawsze używa aktualnej wartości
16 setInterval(() => {
17 setSeconds(prev => Math.max(prev - 1, 0));
18 }, 1000);
19 };
20
21 return (
22 <div>
23 <p>Odliczanie: {seconds}s</p>
24 <button onClick={startCountdownCorrect} disabled={running}>Start</button>
25 </div>
26 );
27}Przycisk z disabled={running} nie pozwoli uruchomić drugiego interwału, a Math.max(prev - 1, 0) zatrzymuje licznik na zerze. Sam interwał działa jednak dalej, dopóki go nie zatrzymasz przez clearInterval - porządne sprzątanie timerów poznasz w module o efektach (useEffect).
Batching - grupowanie aktualizacji
React 18+ automatycznie grupuje (batchuje) wiele aktualizacji stanu w jednym event handlerze, wykonując tylko jeden re-render. To jak komputer pokładowy, który zbiera wszystkie raporty z czujników i aktualizuje wyświetlacz raz, zamiast odświeżać go po każdym czujniku:
1function NavigationUpdate() {
2 const [position, setPosition] = useState({ x: 0, y: 0 });
3 const [velocity, setVelocity] = useState(0);
4 const [status, setStatus] = useState("idle");
5
6 const handleJump = () => {
7 // React grupuje te 3 aktualizacje w jeden render
8 setPosition({ x: 100, y: 200 });
9 setVelocity(9999);
10 setStatus("hyperjump");
11 // Komponent renderuje się JEDEN raz, nie trzy!
12 };
13
14 return (
15 <div>
16 <p>Pozycja: ({position.x}, {position.y})</p>
17 <p>Prędkość: {velocity} | Status: {status}</p>
18 <button onClick={handleJump}>Skok nadprzestrzenny</button>
19 </div>
20 );
21}Praktyczne wzorce aktualizacji stanu
Oto kilka wzorców, które przyda ci się znać na pokładzie statku React:
1// Usuwanie elementu z tablicy
2setItems(prev => prev.filter(item => item.id !== idToRemove));
3
4// Aktualizacja jednego elementu w tablicy
5setItems(prev => prev.map(item =>
6 item.id === targetId ? { ...item, status: "done" } : item
7));
8
9// Toggle (przełączanie boolean)
10setIsActive(prev => !prev);
11
12// Dodanie elementu na początek tablicy
13setMessages(prev => [newMessage, ...prev]);
14
15// Inkrementacja z limitem
16setFuel(prev => Math.min(prev + 10, 100));Leniwa inicjalizacja stanu
Wartość początkowa jest potrzebna tylko przy pierwszym renderze. Jeśli jednak w nawiasie useState wywołasz funkcję, wykona się ona przy każdym renderze, choć React użyje jej wyniku tylko za pierwszym razem:
1// ŹLE - computeStarMap() liczy się przy każdym renderze
2const [stars, setStars] = useState(computeStarMap());Przy kosztownym obliczeniu (np. wyznaczaniu mapy gwiazd z tysięcy punktów) przekaż samą funkcję - React wywoła ją tylko raz, przy pierwszym renderze:
1// DOBRZE - funkcja inicjalizująca wykona się tylko przy pierwszym renderze
2const [stars, setStars] = useState(() => computeStarMap());Przekazujesz funkcję (() => computeStarMap() albo samo computeStarMap), a nie jej wynik - React zapamięta wartość i przy kolejnych renderach nie będzie jej już liczył.
Podsumowanie
Aktualizacja stanu w React - kluczowe zasady:
- Aktualizacja funkcyjna (
prev => prev + 1) - używaj gdy nowa wartość zależy od poprzedniej - Nigdy nie mutuj stanu - zawsze twórz nowe tablice/obiekty (spread operator)
- Batching - React grupuje aktualizacje i renderuje komponent raz
- Stale closures - w callbackach i timerach używaj aktualizacji funkcyjnej
- Wiele useState vs obiekt - zależy od logicznego powiązania danych
- Leniwa inicjalizacja -
useState(() => obliczenie())liczy wartość początkową tylko raz
Kod do tej lekcji: App.jsx
1import React, { useState } from 'react';
2
3function FuelManager() {
4 // Oddzielne zmienne stanu
5 const [fuel, setFuel] = useState(50);
6 const [oxygen, setOxygen] = useState(100);
7
8 // Stan jako obiekt
9 const [position, setPosition] = useState({ x: 0, y: 0, z: 0 });
10
11 // Stan jako tablica
12 const [logs, setLogs] = useState([
13 { id: 1, message: 'System uruchomiony', time: '00:00' }
14 ]);
15
16 const [newLog, setNewLog] = useState('');
17
18 // Aktualizacja funkcyjna (prev =>)
19 const boostFuel = () => {
20 setFuel(prev => Math.min(prev + 10, 100));
21 };
22
23 const consumeFuel = () => {
24 setFuel(prev => Math.max(prev - 15, 0));
25 };
26
27 // Aktualizacja obiektu z spread
28 const moveShip = (axis, delta) => {
29 setPosition(prev => ({ ...prev, [axis]: prev[axis] + delta }));
30 };
31
32 // Dodawanie do tablicy (spread, nie push!)
33 const addLog = () => {
34 if (!newLog.trim()) return;
35 const now = new Date().toLocaleTimeString().slice(0, 5);
36 setLogs(prev => [...prev, {
37 id: Date.now(),
38 message: newLog,
39 time: now
40 }]);
41 setNewLog('');
42 };
43
44 // Usuwanie z tablicy (filter)
45 const removeLog = (id) => {
46 setLogs(prev => prev.filter(log => log.id !== id));
47 };
48
49 // Triple boost - porównanie błędnego i poprawnego podejścia
50 const brokenTripleBoost = () => {
51 // fuel jest "zamrożone" - wszystkie 3 widzą tę samą wartość
52 setFuel(fuel + 5);
53 setFuel(fuel + 5);
54 setFuel(fuel + 5);
55 };
56
57 const correctTripleBoost = () => {
58 // Każde wywołanie widzi aktualną wartość
59 setFuel(prev => Math.min(prev + 5, 100));
60 setFuel(prev => Math.min(prev + 5, 100));
61 setFuel(prev => Math.min(prev + 5, 100));
62 };
63
64 return (
65 <div style={{ fontFamily: 'Arial, sans-serif', padding: '20px', maxWidth: '700px', margin: '0 auto' }}>
66 <h1 style={{ textAlign: 'center', color: '#667eea' }}>Wzorce aktualizacji stanu</h1>
67
68 {/* Sekcja paliwa */}
69 <div style={{ padding: '16px', marginBottom: '16px', backgroundColor: '#f0f4ff', borderRadius: '12px' }}>
70 <h3>Paliwo: {fuel}%</h3>
71 <div style={{ height: '20px', backgroundColor: '#ddd', borderRadius: '10px', overflow: 'hidden' }}>
72 <div style={{
73 height: '100%', width: fuel + '%',
74 backgroundColor: fuel > 30 ? '#4caf50' : '#f44336',
75 transition: 'width 0.3s'
76 }} />
77 </div>
78 <div style={{ marginTop: '10px', display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
79 <button onClick={boostFuel} style={btnStyle('#4caf50')}>+10 (funkcyjny)</button>
80 <button onClick={consumeFuel} style={btnStyle('#f44336')}>-15 (funkcyjny)</button>
81 <button onClick={brokenTripleBoost} style={btnStyle('#ff9800')}>3x +5 (błędny)</button>
82 <button onClick={correctTripleBoost} style={btnStyle('#2196f3')}>3x +5 (poprawny)</button>
83 </div>
84 </div>
85
86 {/* Sekcja pozycji (obiekt) */}
87 <div style={{ padding: '16px', marginBottom: '16px', backgroundColor: '#f0fff4', borderRadius: '12px' }}>
88 <h3>Pozycja statku (obiekt ze spread)</h3>
89 <p>X: {position.x} | Y: {position.y} | Z: {position.z}</p>
90 <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
91 <button onClick={() => moveShip('x', 10)} style={btnStyle('#667eea')}>X +10</button>
92 <button onClick={() => moveShip('y', 10)} style={btnStyle('#667eea')}>Y +10</button>
93 <button onClick={() => moveShip('z', 10)} style={btnStyle('#667eea')}>Z +10</button>
94 <button onClick={() => setPosition({ x: 0, y: 0, z: 0 })} style={btnStyle('#999')}>Reset</button>
95 </div>
96 </div>
97
98 {/* Sekcja logów (tablica) */}
99 <div style={{ padding: '16px', backgroundColor: '#fff8f0', borderRadius: '12px' }}>
100 <h3>Dziennik pokładowy (tablica ze spread)</h3>
101 <div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
102 <input
103 value={newLog}
104 onChange={e => setNewLog(e.target.value)}
105 placeholder="Nowy wpis..."
106 style={{ flex: 1, padding: '8px', borderRadius: '8px', border: '1px solid #ddd' }}
107 />
108 <button onClick={addLog} style={btnStyle('#ff9800')}>Dodaj</button>
109 </div>
110 {logs.map(log => (
111 <div key={log.id} style={{
112 display: 'flex', justifyContent: 'space-between', alignItems: 'center',
113 padding: '8px 12px', marginBottom: '6px', backgroundColor: '#fff',
114 borderRadius: '8px', border: '1px solid #eee'
115 }}>
116 <span><strong>[{log.time}]</strong> {log.message}</span>
117 <button onClick={() => removeLog(log.id)} style={{
118 background: 'none', border: 'none', color: '#f44336', cursor: 'pointer', fontSize: '16px'
119 }}>x</button>
120 </div>
121 ))}
122 </div>
123 </div>
124 );
125}
126
127function btnStyle(bg) {
128 return {
129 padding: '8px 16px', backgroundColor: bg, color: 'white',
130 border: 'none', borderRadius: '8px', cursor: 'pointer', fontSize: '13px'
131 };
132}
133
134export default function App() {
135 return <FuelManager />;
136}Pamiętaj: Stan w React to jak dziennik pokładowy twojego statku - nigdy go nie edytujesz, tylko tworzysz nowy wpis na podstawie poprzedniego!
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 przy wielokrotnym wywołaniu setState w jednym handlerze należy używać aktualizacji funkcyjnej (prev => prev + 1)?
2. Dlaczego poniższy kod jest błędny? const [items, setItems] = useState(['A', 'B']); items.push('C'); setItems(items);
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Układanie w pionie
Uporządkuj, co się dzieje, gdy React grupuje (batching) wiele aktualizacji stanu: