Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Stan komponentu (State)

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.

Czym jest stan komponentu?

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.

Zarządzanie stanem w komponentach funkcyjnych

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.

Podstawowa składnia useState

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:

  1. Importujemy hook
    useState
    z Reacta
  2. Wywołujemy
    useState(100)
    , aby stworzyć zmienną stanu
    fuel
    z wartością początkową 100
  3. Destrukturyzujemy wynik, aby uzyskać:
    • fuel
      : aktualną wartość stanu
    • setFuel
      : funkcję do aktualizacji tej wartości
  4. Używamy
    setFuel
    w handlera zdarzenia kliknięcia, aby zmniejszyć wartość paliwa o 10

Używanie wielu zmiennych stanu

Komponent 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}

Złożone struktury stanu

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.

State jako migawka

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.

Aktualizacja stanu na podstawie poprzedniego stanu

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.

Aktualizacja obiektów w stanie

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}

Aktualizacja tablic w stanie

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}

Kiedy używać state?

Stan powinien być używany dla danych, które:

  1. Zmieniają się w czasie
  2. Wpływają na renderowanie komponentu
  3. Nie mogą być obliczone na podstawie props lub innego stanu

Nie wszystkie dane powinny być w stanie. Dobre pytania, które warto sobie zadać:

  • Czy dane pochodzą od rodzica poprzez props? Jeśli tak, prawdopodobnie nie powinny być w stanie.
  • Czy dane pozostają niezmienione w czasie? Jeśli tak, prawdopodobnie nie powinny być w stanie.
  • Czy dane można obliczyć na podstawie istniejących props lub stanu? Jeśli tak, zdecydowanie nie powinny być w stanie.

Podsumowanie

Stan (state) jest kluczowym elementem komponentów React, który pozwala na:

  • Przechowywanie danych specyficznych dla komponentu
  • Zarządzanie danymi, które zmieniają się w czasie
  • Tworzenie interaktywnych interfejsów użytkownika, które reagują na akcje użytkownika

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.

Przejdź do CodeWorlds