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.
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:dispatch każdemu komponentowi w drzewie bez prop drillingAnalogia 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.
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}Zamiast wywoływać
useContext(MissionContext) w każdym komponencie, tworzymy dedykowany hook. To standardowa konwencja, która: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}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}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.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.