W kosmicznej podróży przez galaktykę React, stan (state) jest jak paliwo rakietowe napędzające interaktywność twoich komponentów. Bez stanu, komponenty byłyby statycznymi obiektami - jak planety bez atmosfery. Stan pozwala komponentom "pamiętać" informacje i reagować na zmiany, czyniąc twoje aplikacje żywymi i responsywnymi.
Stan to obiekt JavaScript, który przechowuje dane specyficzne dla komponentu. W przeciwieństwie do props, które są przekazywane do komponentu, stan jest zarządzany wewnątrz komponentu i może być zmieniany bezpośrednio przez sam komponent.
Wyobraź sobie panel kontrolny statku kosmicznego. Props to jak ustalone współrzędne docelowe - nie możesz ich zmienić z poziomu panelu. Stan natomiast to jak aktualne wskazania prędkości, wysokości, temperatury - dane, które nieustannie się zmieniają i są kontrolowane przez sam panel.
W nowoczesnym React, stan w komponentach funkcyjnych zarządzany jest za pomocą hooka
useState. Ten prosty, ale potężny hook pozwala na dodanie stanu do komponentów funkcyjnych.1import React, { useState } from 'react';
2
3function ControlPanel() {
4 // Deklaracja zmiennej stanu "fuel" z wartością początkową 100
5 const [fuel, setFuel] = useState(100);
6
7 // fuel - aktualna wartość stanu
8 // setFuel - funkcja do aktualizacji stanu
9
10 return (
11 <div className="control-panel">
12 <h2>Panel sterowania</h2>
13 <p>Poziom paliwa: {fuel}%</p>
14 <button onClick={() => setFuel(fuel - 10)}>
15 Zużyj paliwo
16 </button>
17 </div>
18 );
19}W powyższym przykładzie:
useState z ReactauseState(100), aby stworzyć zmienną stanu fuel z wartością początkową 100fuel: aktualną wartość stanusetFuel: funkcję do aktualizacji tej wartościsetFuel w handlera zdarzenia kliknięcia, aby zmniejszyć wartość paliwa o 10Komponent może mieć wiele niezależnych zmiennych stanu:
1function SpaceshipDashboard() {
2 const [fuel, setFuel] = useState(100);
3 const [speed, setSpeed] = useState(0);
4 const [shields, setShields] = useState(true);
5 const [destination, setDestination] = useState('Orbita');
6
7 return (
8 <div className="dashboard">
9 <h2>Pulpit statku kosmicznego</h2>
10 <div className="gauges">
11 <div className="gauge">
12 <h3>Paliwo</h3>
13 <p>{fuel}%</p>
14 <button onClick={() => setFuel(Math.max(0, fuel - 5))}>
15 Zwiększ ciąg
16 </button>
17 </div>
18
19 <div className="gauge">
20 <h3>Prędkość</h3>
21 <p>{speed} km/s</p>
22 <button onClick={() => setSpeed(speed + 1)}>
23 Przyspiesz
24 </button>
25 <button onClick={() => setSpeed(Math.max(0, speed - 1))}>
26 Zwolnij
27 </button>
28 </div>
29
30 <div className="gauge">
31 <h3>Tarcze</h3>
32 <p>{shields ? 'Włączone' : 'Wyłączone'}</p>
33 <button onClick={() => setShields(!shields)}>
34 Przełącz
35 </button>
36 </div>
37
38 <div className="gauge">
39 <h3>Cel podróży</h3>
40 <p>{destination}</p>
41 <select
42 value={destination}
43 onChange={(e) => setDestination(e.target.value)}
44 >
45 <option value="Orbita">Orbita Ziemi</option>
46 <option value="Księżyc">Księżyc</option>
47 <option value="Mars">Mars</option>
48 <option value="Pas asteroid">Pas asteroid</option>
49 </select>
50 </div>
51 </div>
52 </div>
53 );
54}Stan może być prostą wartością (liczba, string, boolean), ale może też być bardziej złożoną strukturą, jak tablica czy obiekt:
1function CrewManagement() {
2 const [crew, setCrew] = useState([
3 { id: 1, name: 'Anna Nowak', role: 'Kapitan', onDuty: true },
4 { id: 2, name: 'Jan Kowalski', role: 'Nawigator', onDuty: false },
5 { id: 3, name: 'Maria Wiśniewska', role: 'Inżynier', onDuty: true }
6 ]);
7
8 const toggleDuty = (crewId) => {
9 setCrew(crew.map(member =>
10 member.id === crewId
11 ? { ...member, onDuty: !member.onDuty }
12 : member
13 ));
14 };
15
16 return (
17 <div className="crew-management">
18 <h2>Zarządzanie załogą</h2>
19 <ul className="crew-list">
20 {crew.map(member => (
21 <li key={member.id} className={member.onDuty ? 'on-duty' : 'off-duty'}>
22 <strong>{member.name}</strong> - {member.role}
23 <button onClick={() => toggleDuty(member.id)}>
24 {member.onDuty ? 'Zakończ służbę' : 'Rozpocznij służbę'}
25 </button>
26 </li>
27 ))}
28 </ul>
29 </div>
30 );
31}W tym przykładzie stan
crew to tablica obiektów. Funkcja toggleDuty używa map do tworzenia nowej tablicy z zaktualizowanym stanem konkretnego członka załogi.W React, state działa jak migawka (snapshot). Kiedy React renderuje komponent, "zatrzaskuje" stan w czasie, a wszystkie wartości wewnątrz funkcji renderującej (w tym handlery zdarzeń) będą "widzieć" tę samą migawkę stanu.
Rozważmy przykład:
1function RocketLauncher() {
2 const [countdown, setCountdown] = useState(10);
3
4 const decrementCountdown = () => {
5 // To NIE zadziała jak oczekujemy, jeśli chcemy
6 // wykonać wielokrotne odliczanie w jednym cyklu renderowania
7 setCountdown(countdown - 1); // Używa tej samej wartości ze snapshota
8 setCountdown(countdown - 1); // Wciąż używa tej samej wartości!
9 };
10
11 return (
12 <div className="rocket-launcher">
13 <h2>Odliczanie do startu: {countdown}</h2>
14 <button onClick={decrementCountdown}>
15 Zmniejsz odliczanie
16 </button>
17 </div>
18 );
19}W powyższym przykładzie, mimo dwukrotnego wywołania
setCountdown, wartość zostanie zmniejszona tylko o 1, a nie o 2. Dzieje się tak, ponieważ obie aktualizacje "widzą" tę samą wartość countdown z migawki.Aby rozwiązać problem migawek, React pozwala na przekazanie funkcji do funkcji aktualizującej stan. Ta funkcja otrzymuje poprzedni stan jako argument:
1function RocketLauncher() {
2 const [countdown, setCountdown] = useState(10);
3
4 const decrementCountdown = () => {
5 // To zadziała poprawnie, nawet dla wielu aktualizacji
6 setCountdown(prevCount => prevCount - 1); // Bazuje na poprzedniej wartości
7 setCountdown(prevCount => prevCount - 1); // Bazuje na wyniku poprzedniej aktualizacji
8 };
9
10 return (
11 <div className="rocket-launcher">
12 <h2>Odliczanie do startu: {countdown}</h2>
13 <button onClick={decrementCountdown}>
14 Zmniejsz odliczanie
15 </button>
16 </div>
17 );
18}Teraz, przy każdym kliknięciu przycisku, odliczanie zostanie zmniejszone o 2.
Przy aktualizacji obiektów w stanie, ważne jest, aby zawsze tworzyć nowy obiekt, a nie modyfikować istniejącego. Jest to zgodne z zasadami immutability w React:
1function SpaceshipSystems() {
2 const [systems, setSystems] = useState({
3 engines: 'offline',
4 lifeSupportOutput: 100,
5 navigationStatus: 'calibrating',
6 shields: 85
7 });
8
9 const startEngines = () => {
10 // ŹLE - modyfikacja oryginalnego obiektu
11 // systems.engines = 'online';
12 // setSystems(systems);
13
14 // DOBRZE - tworzenie nowego obiektu
15 setSystems({
16 ...systems, // kopiuje wszystkie istniejące właściwości
17 engines: 'online' // nadpisuje tylko tę, którą chcemy zmienić
18 });
19 };
20
21 return (
22 <div className="spaceship-systems">
23 <h2>Status systemów</h2>
24 <p>Silniki: {systems.engines}</p>
25 <p>System podtrzymywania życia: {systems.lifeSupportOutput}%</p>
26 <p>Nawigacja: {systems.navigationStatus}</p>
27 <p>Tarcze: {systems.shields}%</p>
28
29 <button onClick={startEngines}>
30 Uruchom silniki
31 </button>
32 </div>
33 );
34}Podobnie jak w przypadku obiektów, przy aktualizacji tablic również należy przestrzegać zasad immutability:
1function MissionLog() {
2 const [logs, setLogs] = useState([
3 { id: 1, time: '08:00', message: 'Rozpoczęcie misji' }
4 ]);
5
6 const addLog = () => {
7 const newLog = {
8 id: logs.length + 1,
9 time: new Date().toLocaleTimeString(),
10 message: 'Nowy wpis w dzienniku'
11 };
12
13 // ŹLE - mutowanie oryginalnej tablicy
14 // logs.push(newLog);
15 // setLogs(logs);
16
17 // DOBRZE - tworzenie nowej tablicy
18 setLogs([...logs, newLog]); // dodawanie na koniec
19 // lub
20 // setLogs([newLog, ...logs]); // dodawanie na początek
21 };
22
23 const removeLog = (logId) => {
24 // Filtrowanie - tworzenie nowej tablicy bez usuwanego elementu
25 setLogs(logs.filter(log => log.id !== logId));
26 };
27
28 return (
29 <div className="mission-log">
30 <h2>Dziennik misji</h2>
31 <button onClick={addLog}>Dodaj nowy wpis</button>
32
33 <ul className="log-entries">
34 {logs.map(log => (
35 <li key={log.id}>
36 <span className="time">{log.time}</span>
37 <span className="message">{log.message}</span>
38 <button onClick={() => removeLog(log.id)}>Usuń</button>
39 </li>
40 ))}
41 </ul>
42 </div>
43 );
44}Stan powinien być używany dla danych, które:
Nie wszystkie dane powinny być w stanie. Dobre pytania, które warto sobie zadać:
Stan (state) jest kluczowym elementem komponentów React, który pozwala na:
W następnych sekcjach zobaczymy, jak obsługiwać zdarzenia w React i jak łączyć je z zarządzaniem stanem, aby tworzyć w pełni interaktywne aplikacje.