We use cookies to enhance your experience on the site
CodeWorlds

Context and useReducer in practice

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
.

Go to CodeWorlds