We use cookies to enhance your experience on the site
CodeWorlds

Optimizing Context API

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.

Go to CodeWorlds