You've already learned Context API and useReducer separately. Now it's time to combine these two tools into one complete state management system - just as a space mission command center connects all communication systems into one unified control panel.
useReducer alone centralizes state logic, but the state remains local to a single component. Context API alone makes data globally available, but managing complex updates through setState becomes messy. Combining both gives us the best of both worlds:dispatch available to every component in the tree without prop drillingSpace analogy: Context is the ship's communication network that reaches every cabin. useReducer is the central computer that processes all commands. Together they form Mission Control - every crew member can send a command and receive the current status, without having to run through corridors with notes.
The key pattern involves placing both
state and dispatch in the context value:1import { createContext, useContext, useReducer } from 'react';
2
3// Create context
4const MissionContext = createContext();
5
6// Reducer with state logic
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 - combines Context with useReducer
21function MissionProvider({ children }) {
22 const initialState = {
23 status: 'docked',
24 crew: ['Captain 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}Instead of calling
useContext(MissionContext) in every component, we create a dedicated hook. This is a standard convention that:1function useMission() {
2 const context = useContext(MissionContext);
3 if (!context) {
4 throw new Error('useMission must be used within a MissionProvider');
5 }
6 return context;
7}Every component inside the provider can now directly send actions - without passing functions through props:
1function StatusPanel() {
2 const { state, dispatch } = useMission();
3
4 return (
5 <div className="status-panel">
6 <h3>Mission status: {state.status}</h3>
7 <button onClick={() => dispatch({ type: 'SET_STATUS', payload: 'launching' })}>
8 Begin launch
9 </button>
10 <button onClick={() => dispatch({ type: 'SET_STATUS', payload: 'orbiting' })}>
11 Enter orbit
12 </button>
13 <button onClick={() => dispatch({ type: 'SET_STATUS', payload: 'docked' })}>
14 Dock
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>Crew ({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="New crew member name"
43 />
44 <button onClick={addMember}>Add to crew</button>
45 </div>
46 );
47}As the application grows, one large reducer becomes hard to maintain. We can split the logic into smaller reducers by domain and combine them in a root reducer:
1// Navigation reducer
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// Onboard systems reducer
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// Root reducer combining both
26function rootReducer(state, action) {
27 return {
28 navigation: navigationReducer(state.navigation, action),
29 systems: systemsReducer(state.systems, action)
30 };
31}
32
33// Provider with root reducer
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}The multiple reducers pattern allows for a clean separation of concerns. Each mini-reducer handles its own area of state, but all actions flow through a single
dispatch. This way a TOGGLE_SHIELDS action can simultaneously affect systems and navigation, if needed.Below you can see all the elements brought together - from context definition, through the provider, to components using
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// Component A - control panel
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: 'Mission started!' });
46 }}
47 disabled={state.status === 'flying'}
48 >
49 Launch
50 </button>
51 </div>
52 );
53}
54
55// Component B - state display (separate tree branch)
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// Application
66function App() {
67 return (
68 <MissionProvider>
69 <ControlPanel />
70 <MissionLog />
71 </MissionProvider>
72 );
73}Notice that
ControlPanel and MissionLog can be at different depths in the component tree and don't need to know about each other - they are only connected by the shared MissionProvider.