Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

useReducer - zaawansowane zarządzanie stanem

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 stan
  • initialState
    - 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:

  1. Składnia:
    const [state, dispatch] = useReducer(reducer, initialState)
  2. Reducer: czysta funkcja
    (state, action) => newState
    z wzorcem switch-case
  3. Dispatch: wysyłanie akcji
    dispatch({ type: 'ACTION', payload: data })
  4. Kiedy używać: złożony stan, powiązane aktualizacje, logika biznesowa w stanie
  5. 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.

Vai a CodeWorlds