Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Context i useReducer w praktyce

Poznałeś już Context API i useReducer osobno. Teraz czas połączyć te dwa narzędzia w jeden kompletny system zarządzania stanem - tak jak centrum dowodzenia misji kosmicznej łączy wszystkie systemy komunikacji w jeden spójny panel sterowania.

Dlaczego łączyć Context z useReducer?

Samo

useReducer
centralizuje logikę stanu, ale stan pozostaje lokalny w jednym komponencie. Samo
Context API
udostępnia dane globalnie, ale zarządzanie złożonymi aktualizacjami przez
setState
staje się nieuporządkowane. Połączenie obu daje nam najlepsze z dwóch światów:

  • Context - udostępnia stan i
    dispatch
    każdemu komponentowi w drzewie bez prop drilling
  • useReducer - porządkuje całą logikę aktualizacji w jednej, testowalnej funkcji

Analogia kosmiczna: Context to sieć komunikacyjna statku, która dociera do każdej kabiny. useReducer to centralny komputer, który przetwarza wszystkie rozkazy. Razem tworzą Mission Control - każdy członek załogi może wysłać rozkaz i otrzymać aktualny status, bez konieczności biegania po korytarzach z karteczkami.

Wzorzec: kontekst ze stanem i dispatch

Kluczowy wzorzec polega na umieszczeniu zarówno

state
, jak i
dispatch
w wartości kontekstu:

1import { createContext, useContext, useReducer } from 'react';
2
3// Tworzymy kontekst
4const MissionContext = createContext();
5
6// Reducer z logiką stanu
7function missionReducer(state, action) {
8  switch (action.type) {
9    case 'SET_STATUS':
10      return { ...state, status: action.payload };
11    case 'ADD_CREW':
12      return { ...state, crew: [...state.crew, action.payload] };
13    case 'UPDATE_FUEL':
14      return { ...state, fuel: Math.max(0, Math.min(100, action.payload)) };
15    default:
16      return state;
17  }
18}
19
20// Provider - łączy Context z useReducer
21function MissionProvider({ children }) {
22  const initialState = {
23    status: 'docked',
24    crew: ['Kapitan Nova'],
25    fuel: 100
26  };
27
28  const [state, dispatch] = useReducer(missionReducer, initialState);
29
30  return (
31    <MissionContext.Provider value={{ state, dispatch }}>
32      {children}
33    </MissionContext.Provider>
34  );
35}

Niestandardowy hook dostępu do kontekstu

Zamiast wywoływać

useContext(MissionContext)
w każdym komponencie, tworzymy dedykowany hook. To standardowa konwencja, która:

  • Chroni przed użyciem kontekstu poza providerem (zgłasza czytelny błąd)
  • Upraszcza importy w komponentach
  • Ułatwia przyszłe refaktoryzacje
1function useMission() {
2  const context = useContext(MissionContext);
3  if (!context) {
4    throw new Error('useMission musi być używany wewnątrz MissionProvider');
5  }
6  return context;
7}

Używanie dispatch z komponentów potomnych

Każdy komponent wewnątrz providera może teraz bezpośrednio wysyłać akcje - bez przekazywania funkcji przez props:

1function StatusPanel() {
2  const { state, dispatch } = useMission();
3
4  return (
5    <div className="status-panel">
6      <h3>Status misji: {state.status}</h3>
7      <button onClick={() => dispatch({ type: 'SET_STATUS', payload: 'launching' })}>
8        Rozpocznij start
9      </button>
10      <button onClick={() => dispatch({ type: 'SET_STATUS', payload: 'orbiting' })}>
11        Wejdź na orbitę
12      </button>
13      <button onClick={() => dispatch({ type: 'SET_STATUS', payload: 'docked' })}>
14        Dokuj
15      </button>
16    </div>
17  );
18}
19
20function CrewPanel() {
21  const { state, dispatch } = useMission();
22  const [newMember, setNewMember] = React.useState('');
23
24  const addMember = () => {
25    if (newMember.trim()) {
26      dispatch({ type: 'ADD_CREW', payload: newMember.trim() });
27      setNewMember('');
28    }
29  };
30
31  return (
32    <div className="crew-panel">
33      <h3>Załoga ({state.crew.length}):</h3>
34      <ul>
35        {state.crew.map((member, i) => (
36          <li key={i}>{member}</li>
37        ))}
38      </ul>
39      <input
40        value={newMember}
41        onChange={(e) => setNewMember(e.target.value)}
42        placeholder="Imię nowego członka załogi"
43      />
44      <button onClick={addMember}>Dodaj do załogi</button>
45    </div>
46  );
47}

Wzorzec wielu reducerów

Gdy aplikacja rośnie, jeden duży reducer staje się trudny w utrzymaniu. Możemy podzielić logikę na mniejsze reduktory według domeny i połączyć je w głównym reducerze:

1// Reduktor dla systemu nawigacji
2function navigationReducer(navState, action) {
3  switch (action.type) {
4    case 'SET_DESTINATION':
5      return { ...navState, destination: action.payload, status: 'en route' };
6    case 'ARRIVE':
7      return { ...navState, location: navState.destination, destination: null, status: 'arrived' };
8    default:
9      return navState;
10  }
11}
12
13// Reduktor dla systemów pokładowych
14function systemsReducer(systemsState, action) {
15  switch (action.type) {
16    case 'TOGGLE_SHIELDS':
17      return { ...systemsState, shields: !systemsState.shields };
18    case 'SET_ENGINE_POWER':
19      return { ...systemsState, enginePower: action.payload };
20    default:
21      return systemsState;
22  }
23}
24
25// Główny reduktor łączący oba
26function rootReducer(state, action) {
27  return {
28    navigation: navigationReducer(state.navigation, action),
29    systems: systemsReducer(state.systems, action)
30  };
31}
32
33// Provider z głównym reduktorem
34function SpaceshipProvider({ children }) {
35  const initialState = {
36    navigation: { location: 'Earth', destination: null, status: 'docked' },
37    systems: { shields: false, enginePower: 0 }
38  };
39
40  const [state, dispatch] = useReducer(rootReducer, initialState);
41
42  return (
43    <SpaceshipContext.Provider value={{ state, dispatch }}>
44      {children}
45    </SpaceshipContext.Provider>
46  );
47}

Wzorzec wielu reducerów pozwala na czysty podział odpowiedzialności. Każdy mini-reduktor obsługuje swój obszar stanu, ale wszystkie akcje przepływają przez jeden

dispatch
. Dzięki temu akcja
TOGGLE_SHIELDS
może jednocześnie wpłynąć na systemy i nawigację, jeśli tego potrzebujemy.

Kompletna aplikacja Mission Control

Poniżej widzisz wszystkie elementy zebrane razem - od definicji kontekstu, przez provider, po komponenty używające

useMission
:

1import { createContext, useContext, useReducer } from 'react';
2
3const MissionContext = createContext();
4
5function missionReducer(state, action) {
6  switch (action.type) {
7    case 'LAUNCH':
8      return { ...state, status: 'flying', fuel: state.fuel - 20 };
9    case 'DOCK':
10      return { ...state, status: 'docked', speed: 0 };
11    case 'ADD_LOG':
12      return { ...state, log: [...state.log, action.payload] };
13    default:
14      return state;
15  }
16}
17
18function MissionProvider({ children }) {
19  const [state, dispatch] = useReducer(missionReducer, {
20    status: 'docked',
21    fuel: 100,
22    speed: 0,
23    log: []
24  });
25
26  return (
27    <MissionContext.Provider value={{ state, dispatch }}>
28      {children}
29    </MissionContext.Provider>
30  );
31}
32
33function useMission() {
34  return useContext(MissionContext);
35}
36
37// Komponent A - panel sterowania
38function ControlPanel() {
39  const { state, dispatch } = useMission();
40  return (
41    <div>
42      <button
43        onClick={() => {
44          dispatch({ type: 'LAUNCH' });
45          dispatch({ type: 'ADD_LOG', payload: 'Start misji!' });
46        }}
47        disabled={state.status === 'flying'}
48      >
49        Start
50      </button>
51    </div>
52  );
53}
54
55// Komponent B - wyświetlanie stanu (osobna gałąź drzewa)
56function MissionLog() {
57  const { state } = useMission();
58  return (
59    <ul>
60      {state.log.map((entry, i) => <li key={i}>{entry}</li>)}
61    </ul>
62  );
63}
64
65// Aplikacja
66function App() {
67  return (
68    <MissionProvider>
69      <ControlPanel />
70      <MissionLog />
71    </MissionProvider>
72  );
73}

Zauważ, że

ControlPanel
i
MissionLog
mogą znajdować się na różnych głębokościach drzewa komponentów i nie muszą o sobie nawzajem wiedzieć - łączy je tylko wspólny
MissionProvider
.

Ir a CodeWorlds