JavaScript and React course Β· Module 4: Context API and useReducer
Context and useReducer in practice
In this lesson6
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.
Why combine Context with useReducer?
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:
- Context - makes state and
dispatchavailable to every component in the tree without prop drilling - useReducer - organizes all update logic in a single, testable function
Space 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.
Pattern: context with state and dispatch
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}Custom context access hook
Instead of calling useContext(MissionContext) in every component, we create a dedicated hook. This is a standard convention that:
- Protects against using context outside the provider (throws a readable error)
- Simplifies imports in components
- Facilitates future refactoring
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}Using dispatch from child components
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}Multiple reducers pattern
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.
Complete Mission Control application
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.
Code for this lesson: App.jsx
1import React, { createContext, useContext, useReducer } from 'react';
2
3// --- Mission reducer ---
4const initialState = {
5 missions: [
6 { id: 1, name: 'Mars Exploration', priority: 'high', completed: false },
7 { id: 2, name: 'Jupiter Probe', priority: 'medium', completed: false },
8 ],
9};
10
11function missionReducer(state, action) {
12 switch (action.type) {
13 case 'ADD_MISSION':
14 return {
15 ...state,
16 missions: [
17 ...state.missions,
18 { id: Date.now(), name: action.name, priority: action.priority, completed: false },
19 ],
20 };
21 case 'COMPLETE_MISSION':
22 return {
23 ...state,
24 missions: state.missions.map(m =>
25 m.id === action.id ? { ...m, completed: true } : m
26 ),
27 };
28 case 'SET_PRIORITY':
29 return {
30 ...state,
31 missions: state.missions.map(m =>
32 m.id === action.id ? { ...m, priority: action.priority } : m
33 ),
34 };
35 default:
36 return state;
37 }
38}
39
40// --- Context ---
41const MissionContext = createContext();
42
43function MissionProvider({ children }) {
44 const [state, dispatch] = useReducer(missionReducer, initialState);
45 return (
46 <MissionContext.Provider value={{ state, dispatch }}>
47 {children}
48 </MissionContext.Provider>
49 );
50}
51
52function useMission() {
53 return useContext(MissionContext);
54}
55
56// --- Components ---
57const priorityColors = { high: '#f44336', medium: '#ff9800', low: '#4caf50' };
58
59function MissionList() {
60 const { state, dispatch } = useMission();
61 return (
62 <div style={panel}>
63 <h2 style={{ color: '#4fc3f7', marginTop: 0 }}>Mission list</h2>
64 {state.missions.map(m => (
65 <div key={m.id} style={{ ...missionItem, opacity: m.completed ? 0.5 : 1 }}>
66 <span style={{ flex: 1 }}>{m.name}</span>
67 <span style={{ color: priorityColors[m.priority], marginRight: 8 }}>
68 [{m.priority}]
69 </span>
70 {!m.completed && (
71 <>
72 <select
73 value={m.priority}
74 onChange={e => dispatch({ type: 'SET_PRIORITY', id: m.id, priority: e.target.value })}
75 style={selectStyle}
76 >
77 <option value="high">High</option>
78 <option value="medium">Medium</option>
79 <option value="low">Low</option>
80 </select>
81 <button
82 onClick={() => dispatch({ type: 'COMPLETE_MISSION', id: m.id })}
83 style={btnStyle}
84 >
85 Complete
86 </button>
87 </>
88 )}
89 {m.completed && <span style={{ color: '#4caf50' }}>Completed</span>}
90 </div>
91 ))}
92 </div>
93 );
94}
95
96function AddMission() {
97 const { dispatch } = useMission();
98 const [name, setName] = React.useState('');
99 const [priority, setPriority] = React.useState('medium');
100
101 const handleAdd = () => {
102 if (!name.trim()) return;
103 dispatch({ type: 'ADD_MISSION', name, priority });
104 setName('');
105 };
106
107 return (
108 <div style={panel}>
109 <h2 style={{ color: '#4fc3f7', marginTop: 0 }}>New mission</h2>
110 <input
111 value={name}
112 onChange={e => setName(e.target.value)}
113 placeholder="Mission name..."
114 style={inputStyle}
115 />
116 <select value={priority} onChange={e => setPriority(e.target.value)} style={selectStyle}>
117 <option value="high">High priority</option>
118 <option value="medium">Medium priority</option>
119 <option value="low">Low priority</option>
120 </select>
121 <button onClick={handleAdd} style={{ ...btnStyle, background: '#4fc3f7', color: '#0a0e1a' }}>
122 Add mission
123 </button>
124 </div>
125 );
126}
127
128function MissionStats() {
129 const { state } = useMission();
130 const total = state.missions.length;
131 const done = state.missions.filter(m => m.completed).length;
132 return (
133 <div style={{ ...panel, display: 'flex', gap: 20 }}>
134 <span style={{ color: '#adb5bd' }}>Total: <b style={{ color: 'white' }}>{total}</b></span>
135 <span style={{ color: '#adb5bd' }}>Completed: <b style={{ color: '#4caf50' }}>{done}</b></span>
136 <span style={{ color: '#adb5bd' }}>Active: <b style={{ color: '#4fc3f7' }}>{total - done}</b></span>
137 </div>
138 );
139}
140
141export default function App() {
142 return (
143 <MissionProvider>
144 <div style={{ minHeight: '100vh', background: '#0a0e1a', padding: 24, fontFamily: 'Arial', color: 'white' }}>
145 <h1 style={{ textAlign: 'center', color: '#ffd700', marginBottom: 24 }}>
146 Mission Command Center
147 </h1>
148 <MissionStats />
149 <AddMission />
150 <MissionList />
151 </div>
152 </MissionProvider>
153 );
154}
155
156// --- Style ---
157const panel = {
158 background: '#1a2035',
159 borderRadius: 10,
160 padding: 16,
161 marginBottom: 16,
162 border: '1px solid rgba(79,195,247,0.2)',
163};
164const missionItem = {
165 display: 'flex', alignItems: 'center', gap: 8,
166 padding: '8px 0', borderBottom: '1px solid rgba(255,255,255,0.07)',
167};
168const btnStyle = {
169 background: 'rgba(255,255,255,0.1)', color: 'white',
170 border: '1px solid rgba(255,255,255,0.2)', borderRadius: 6,
171 padding: '4px 10px', cursor: 'pointer',
172};
173const selectStyle = {
174 background: '#0a0e1a', color: 'white',
175 border: '1px solid rgba(79,195,247,0.3)', borderRadius: 6, padding: '4px 6px',
176};
177const inputStyle = {
178 background: '#0a0e1a', color: 'white',
179 border: '1px solid rgba(79,195,247,0.3)', borderRadius: 6,
180 padding: '6px 10px', width: '100%', marginBottom: 8, boxSizing: 'border-box',
181};Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Why must reducer functions be 'pure functions' and not mutate state?
2. How do you implement lazy initialization in useReducer?
Hands-on tasks in the game
- Vertical ordering
Arrange the state complexity progression - from simple to requiring useReducer:
- Horizontal ordering
Arrange the case syntax elements in a reducer from left to right:
- Code editor
Implement an intergalactic portal
- Click in order
Arrange the structure of a space reducer:
- Code editor
Build a galactic shopping center