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

Optimizing Context API

5 min read
In this lesson6

A mission command center handles hundreds of signals per second. If every signal caused a restart of all systems - the ship wouldn't make it to orbit. React works similarly: when a context value changes, every component consuming that context is re-rendered. In small applications this goes unnoticed, but in complex systems it can significantly slow things down.

The problem: excessive re-rendering with context

Consider a simple example. We have a single context storing the entire ship state:

1const ShipContext = createContext();
2
3function ShipProvider({ children }) {
4  const [state, dispatch] = useReducer(shipReducer, {
5    status: 'docked',
6    fuel: 100,
7    crewCount: 5,
8    alerts: []
9  });
10
11  return (
12    <ShipContext.Provider value={{ state, dispatch }}>
13      {children}
14    </ShipContext.Provider>
15  );
16}
17
18function FuelGauge() {
19  const { state } = useContext(ShipContext);
20  console.log('FuelGauge is rendering');
21  return <p>Fuel: {state.fuel}%</p>;
22}
23
24function AlertBell() {
25  const { state } = useContext(ShipContext);
26  console.log('AlertBell is rendering');
27  return <p>Alerts: {state.alerts.length}</p>;
28}

When alerts changes (e.g., a new alert), FuelGauge also re-renders - even though the fuel level hasn't changed. Any change to any field in state triggers rendering of all context consumers.

Solution 1: Split context into state and dispatch

The simplest and most effective optimization is splitting the context into two separate ones: one for state (which changes frequently), another for dispatch (which is stable throughout the component's lifetime):

1const ShipStateContext = createContext();
2const ShipDispatchContext = createContext();
3
4function ShipProvider({ children }) {
5  const [state, dispatch] = useReducer(shipReducer, initialState);
6
7  return (
8    <ShipDispatchContext.Provider value={dispatch}>
9      <ShipStateContext.Provider value={state}>
10        {children}
11      </ShipStateContext.Provider>
12    </ShipDispatchContext.Provider>
13  );
14}
15
16// Dedicated hooks for each context
17function useShipState() {
18  return useContext(ShipStateContext);
19}
20
21function useShipDispatch() {
22  return useContext(ShipDispatchContext);
23}

Why does this work? The dispatch returned by useReducer is a stable reference - React guarantees that it doesn't change between renders. Components that only send actions (and don't display state) won't be re-rendered when state changes:

1// This component re-renders only when state changes
2function FuelGauge() {
3  const state = useShipState();
4  return <p>Fuel: {state.fuel}%</p>;
5}
6
7// This component does NOT re-render when ship state changes
8// (because it only uses dispatch, and dispatch is stable)
9function LaunchButton() {
10  const dispatch = useShipDispatch();
11  return (
12    <button onClick={() => dispatch({ type: 'LAUNCH' })}>
13      Launch
14    </button>
15  );
16}

Solution 2: Split into multiple specialized contexts

Instead of one context for the entire state, we can create separate contexts for different functional areas. Components subscribe only to the context that concerns them:

1// Separate context for fuel
2const FuelContext = createContext();
3
4function FuelProvider({ children }) {
5  const [fuel, setFuel] = useState(100);
6  const refuel = () => setFuel(100);
7  const consumeFuel = (amount) => setFuel(prev => Math.max(0, prev - amount));
8
9  return (
10    <FuelContext.Provider value={{ fuel, refuel, consumeFuel }}>
11      {children}
12    </FuelContext.Provider>
13  );
14}
15
16// Separate context for alerts
17const AlertContext = createContext();
18
19function AlertProvider({ children }) {
20  const [alerts, setAlerts] = useState([]);
21  const addAlert = (msg) => setAlerts(prev => [...prev, { id: Date.now(), msg }]);
22  const dismissAlert = (id) => setAlerts(prev => prev.filter(a => a.id !== id));
23
24  return (
25    <AlertContext.Provider value={{ alerts, addAlert, dismissAlert }}>
26      {children}
27    </AlertContext.Provider>
28  );
29}
30
31// FuelGauge re-renders only when fuel changes
32function FuelGauge() {
33  const { fuel } = useContext(FuelContext);
34  return <p>Fuel: {fuel}%</p>;
35}
36
37// AlertBell re-renders only when alerts change
38function AlertBell() {
39  const { alerts } = useContext(AlertContext);
40  return <p>Alerts: {alerts.length}</p>;
41}

Solution 3: React.memo for child components

React.memo is a HOC (Higher-Order Component) that wraps a component and ensures it re-renders only when its props have changed. This is particularly useful for child components of providers:

1// Without memo - re-renders every time the parent re-renders
2function CrewMember({ name, role }) {
3  console.log(`Rendering: ${name}`);
4  return <li>{name} - {role}</li>;
5}
6
7// With memo - re-renders only when name or role change
8const CrewMemberOptimized = React.memo(function CrewMember({ name, role }) {
9  console.log(`Rendering: ${name}`);
10  return <li>{name} - {role}</li>;
11});
12
13function CrewList() {
14  const { state } = useShipState();
15  return (
16    <ul>
17      {state.crew.map(member => (
18        // CrewMemberOptimized doesn't re-render when fuel or status changes
19        <CrewMemberOptimized
20          key={member.id}
21          name={member.name}
22          role={member.role}
23        />
24      ))}
25    </ul>
26  );
27}

When Context, when props?

Not all data should go into context. Here's a practical decision guide:

Use Context when:

  • Data is needed in many components at different levels of the tree
  • You're avoiding passing props through more than 2-3 levels (prop drilling)
  • Data changes rarely or concerns the broad application context (theme, language, user data)
1// Good candidates for Context
2const ThemeContext = createContext();        // theme (dark/light)
3const AuthContext = createContext();         // logged-in user
4const LanguageContext = createContext();     // interface language

Use props when:

  • Data flows through only 1-2 levels of components
  • The component is used multiple times in different contexts (reusable)
  • Data is specific to a particular UI section
1// Better as props - this is component-specific data
2function MissionCard({ mission, onSelect, isActive }) {
3  return (
4    <div className={isActive ? 'active' : ''} onClick={() => onSelect(mission)}>
5      <h3>{mission.name}</h3>
6      <p>{mission.status}</p>
7    </div>
8  );
9}

Practical rule

Before adding something to context, ask yourself: "Do I really need this globally, or is it just prop drilling that I can solve with better component structure?"

1// Sometimes the prop drilling problem can be solved through composition
2// instead of Context:
3
4// Instead of passing onSelectMission through 3 levels...
5function MissionControl() {
6  const [selected, setSelected] = useState(null);
7
8  return (
9    // Pass a ready component as children, instead of a function as a prop
10    <MissionList
11      renderItem={(mission) => (
12        <MissionCard
13          mission={mission}
14          isActive={selected?.id === mission.id}
15          onSelect={setSelected}
16        />
17      )}
18    />
19  );
20}

Context optimization is a balance between convenience and performance. For most applications, a basic split into a state context and a dispatch context is sufficient. More advanced techniques are worth applying when you actually observe performance issues.

Code for this lesson: App.jsx
1import React, { createContext, useContext, useReducer, memo } from 'react';
2
3// --- Separate contexts for state and dispatch ---
4// Optimization: components that only dispatch don't re-render on state change
5const StateContext = createContext();
6const DispatchContext = createContext();
7
8const initialState = {
9  ships: [
10    { id: 1, name: 'Apollo VII', fuel: 80, active: true },
11    { id: 2, name: 'Orion II', fuel: 45, active: false },
12    { id: 3, name: 'Hermes', fuel: 100, active: true },
13  ],
14  selectedId: null,
15};
16
17function reducer(state, action) {
18  switch (action.type) {
19    case 'SELECT_SHIP':
20      return { ...state, selectedId: action.id };
21    case 'REFUEL':
22      return {
23        ...state,
24        ships: state.ships.map(s =>
25          s.id === action.id ? { ...s, fuel: Math.min(100, s.fuel + 20) } : s
26        ),
27      };
28    case 'TOGGLE_ACTIVE':
29      return {
30        ...state,
31        ships: state.ships.map(s =>
32          s.id === action.id ? { ...s, active: !s.active } : s
33        ),
34      };
35    default:
36      return state;
37  }
38}
39
40function FleetProvider({ children }) {
41  const [state, dispatch] = useReducer(reducer, initialState);
42  return (
43    <StateContext.Provider value={state}>
44      <DispatchContext.Provider value={dispatch}>
45        {children}
46      </DispatchContext.Provider>
47    </StateContext.Provider>
48  );
49}
50
51// Context access hooks
52function useFleetState() { return useContext(StateContext); }
53function useFleetDispatch() { return useContext(DispatchContext); }
54
55// --- Components optimized with React.memo ---
56
57// This component re-renders ONLY when a specific ship changes
58const ShipCard = memo(function ShipCard({ ship }) {
59  const dispatch = useFleetDispatch();
60  console.log(`[RENDER] ShipCard: ${ship.name}`);
61  return (
62    <div style={{ ...cardStyle, border: ship.active ? '1px solid #4fc3f7' : '1px solid #333' }}>
63      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
64        <span style={{ color: ship.active ? '#4fc3f7' : '#888', fontWeight: 'bold' }}>
65          {ship.name}
66        </span>
67        <span style={{ fontSize: 12, color: ship.active ? '#4caf50' : '#f44336' }}>
68          {ship.active ? '● ACTIVE' : 'β—‹ INACTIVE'}
69        </span>
70      </div>
71      <div style={{ margin: '8px 0' }}>
72        <div style={{ fontSize: 12, color: '#adb5bd', marginBottom: 4 }}>
73          Fuel: {ship.fuel}%
74        </div>
75        <div style={{ background: '#0a0e1a', borderRadius: 4, height: 8 }}>
76          <div style={{
77            width: `${ship.fuel}%`, height: '100%', borderRadius: 4,
78            background: ship.fuel > 50 ? '#4caf50' : ship.fuel > 25 ? '#ff9800' : '#f44336',
79          }} />
80        </div>
81      </div>
82      <div style={{ display: 'flex', gap: 6 }}>
83        <button onClick={() => dispatch({ type: 'REFUEL', id: ship.id })} style={btnStyle}>
84          Refuel +20%
85        </button>
86        <button onClick={() => dispatch({ type: 'TOGGLE_ACTIVE', id: ship.id })} style={btnStyle}>
87          {ship.active ? 'Deactivate' : 'Activate'}
88        </button>
89      </div>
90    </div>
91  );
92});
93
94// This component re-renders on every state change (reads entire state)
95const FleetStats = memo(function FleetStats() {
96  const { ships } = useFleetState();
97  console.log('[RENDER] FleetStats');
98  const active = ships.filter(s => s.active).length;
99  const avgFuel = Math.round(ships.reduce((sum, s) => sum + s.fuel, 0) / ships.length);
100  return (
101    <div style={{ ...cardStyle, display: 'flex', gap: 20, justifyContent: 'center' }}>
102      <span style={{ color: '#adb5bd' }}>Active: <b style={{ color: '#4fc3f7' }}>{active}/{ships.length}</b></span>
103      <span style={{ color: '#adb5bd' }}>Avg. fuel: <b style={{ color: '#ffd700' }}>{avgFuel}%</b></span>
104    </div>
105  );
106});
107
108// This component only dispatches - does NOT re-render on state change
109const ResetButton = memo(function ResetButton() {
110  const dispatch = useFleetDispatch();
111  console.log('[RENDER] ResetButton - should render only once!');
112  return (
113    <button
114      onClick={() => dispatch({ type: 'RESET' })}
115      style={{ ...btnStyle, width: '100%', padding: '10px', marginTop: 8 }}
116    >
117      Reset fleet
118    </button>
119  );
120});
121
122function ShipList() {
123  const { ships } = useFleetState();
124  console.log('[RENDER] ShipList');
125  return (
126    <div>
127      {ships.map(ship => (
128        <ShipCard key={ship.id} ship={ship} />
129      ))}
130    </div>
131  );
132}
133
134export default function App() {
135  return (
136    <FleetProvider>
137      <div style={{ minHeight: '100vh', background: '#0a0e1a', padding: 24, fontFamily: 'Arial', color: 'white' }}>
138        <h1 style={{ textAlign: 'center', color: '#ffd700', marginBottom: 8 }}>
139          Context API Optimization
140        </h1>
141        <p style={{ textAlign: 'center', color: '#778da9', marginBottom: 20, fontSize: 13 }}>
142          Open console (F12) to see which components re-render
143        </p>
144        <FleetStats />
145        <ShipList />
146        <ResetButton />
147      </div>
148    </FleetProvider>
149  );
150}
151
152const cardStyle = {
153  background: '#1a2035', borderRadius: 10, padding: 14,
154  marginBottom: 12, border: '1px solid rgba(255,255,255,0.1)',
155};
156const btnStyle = {
157  background: 'rgba(79,195,247,0.1)', color: '#4fc3f7',
158  border: '1px solid rgba(79,195,247,0.3)', borderRadius: 6,
159  padding: '5px 10px', cursor: 'pointer', fontSize: 12,
160};

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. How can React.memo help optimize components using Context API?

  2. 2. How do you prevent unnecessary re-renders in components using Context API?

These are 2 of 8 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Create a space university system

  • Code editor

    Implement a genetic laboratory

  • Code editor

    Build a galactic social network

  • Vertical ordering

    Arrange the Provider nesting in a typical React application - from outermost:

  • Code editor

    Build a Context API system with useReducer for fleet management: ThemeContext, FleetContext, dispatch actions, and custom hooks

  • Horizontal ordering

    Arrange a correct dispatch call with an action object:

  • Vertical ordering

    Arrange when to switch from props to Context API - from simplest case:

  • Click in order

    Arrange the code for a space Provider:

  • Vertical ordering

    Arrange the Providers in a space application hierarchy:

  • Vertical ordering

    Arrange the lifecycle events during space context initialization:

  • Click in order

    Arrange the custom hook for context:

  • Horizontal ordering

    Arrange the syntax for creating a context in React:

  • Click in order

    Arrange the space action creators:

  • Horizontal ordering

    Arrange the syntax for creating a new context with a default value:

  • Vertical ordering

    Arrange the steps for testing components with Context API:

  • Horizontal ordering

    Arrange the syntax for initializing useReducer with a reducer and initial state:

  • Vertical ordering

    Arrange the process of migrating old space systems to Context API:

Useful articles