JavaScript and React course Β· Module 4: Context API and useReducer

Context and useReducer in practice

5 min read
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 dispatch available 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. 1. Why must reducer functions be 'pure functions' and not mutate state?

  2. 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

Useful articles