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

Aktualizacja stanu — wzorce i pułapki

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
4  const startCountdown = () => {
5    // ❌ seconds jest "zamrożone" z chwili kliknięcia!
6    // Każdy interwał widzi tę samą wartość
7    setInterval(() => {
8      setSeconds(seconds - 1); // zawsze 10 - 1 = 9
9    }, 1000);
10  };
11
12  const startCountdownCorrect = () => {
13    // ✅ Aktualizacja funkcyjna zawsze używa aktualnej wartości
14    setInterval(() => {
15      setSeconds(prev => prev - 1);
16    }, 1000);
17  };
18
19  return (
20    <div>
21      <p>Odliczanie: {seconds}s</p>
22      <button onClick={startCountdownCorrect}>Start</button>
23    </div>
24  );
25}

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));

Podsumowanie

Aktualizacja stanu w React — kluczowe zasady:

  1. Aktualizacja funkcyjna (
    prev => prev + 1
    ) — używaj gdy nowa wartość zależy od poprzedniej
  2. Nigdy nie mutuj stanu — zawsze twórz nowe tablice/obiekty (spread operator)
  3. Batching — React grupuje aktualizacje i renderuje komponent raz
  4. Stale closures — w callbackach i timerach używaj aktualizacji funkcyjnej
  5. Wiele useState vs obiekt — zależy od logicznego powiązania danych

Pamiętaj: Stan w React to jak dziennik pokładowy twojego statku — nigdy go nie edytujesz, tylko tworzysz nowy wpis na podstawie poprzedniego!

Przejdź do CodeWorlds