patternjavascriptreactModerate
React context performance — split state from dispatch
Viewed 0 times
context performancesplit contextdispatch contextuseReducerstable referencecontext optimization
Problem
Putting both state and the dispatch/setter function in the same context value causes every consumer to re-render whenever state changes — even components that only call setters and never read the state value.
Solution
Separate state context from dispatch context so setter-only consumers don't re-render:
import { createContext, useContext, useReducer } from 'react';
const CountStateContext = createContext(null);
const CountDispatchContext = createContext(null);
function CountProvider({ children }) {
const [count, dispatch] = useReducer(reducer, 0);
// dispatch is stable — never changes between renders
return (
<CountStateContext.Provider value={count}>
<CountDispatchContext.Provider value={dispatch}>
{children}
</CountDispatchContext.Provider>
</CountStateContext.Provider>
);
}
// Components that only increment — never re-render on count changes
function IncrementButton() {
const dispatch = useContext(CountDispatchContext); // stable reference
return <button onClick={() => dispatch({ type: 'increment' })}>+</button>;
}
// Components that display — re-render when count changes
function Display() {
const count = useContext(CountStateContext);
return <p>{count}</p>;
}
import { createContext, useContext, useReducer } from 'react';
const CountStateContext = createContext(null);
const CountDispatchContext = createContext(null);
function CountProvider({ children }) {
const [count, dispatch] = useReducer(reducer, 0);
// dispatch is stable — never changes between renders
return (
<CountStateContext.Provider value={count}>
<CountDispatchContext.Provider value={dispatch}>
{children}
</CountDispatchContext.Provider>
</CountStateContext.Provider>
);
}
// Components that only increment — never re-render on count changes
function IncrementButton() {
const dispatch = useContext(CountDispatchContext); // stable reference
return <button onClick={() => dispatch({ type: 'increment' })}>+</button>;
}
// Components that display — re-render when count changes
function Display() {
const count = useContext(CountStateContext);
return <p>{count}</p>;
}
Why
useReducer's dispatch function is guaranteed stable across renders. By putting dispatch in its own context, IncrementButton only re-renders when the dispatch context value changes — which it never does. This is the same pattern used by React-Redux (actions and state are separate).
Gotchas
- This pattern pairs well with useReducer — dispatch is stable, state is a new reference on each update
- For useState, setters are also stable — split them the same way
- Don't split every context by default — only where you've measured unnecessary re-renders
- A third-party state manager (Zustand, Jotai) gives you selector-level subscriptions without this manual split
Code Snippets
Split state and dispatch contexts
// State and dispatch in separate contexts
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>Context
When a context object contains both state and setters and components that only set state are re-rendering unnecessarily
Revisions (0)
No revisions yet.