React Context Performance
Eliminate wasteful consumer re-renders by splitting context values, memoizing providers, and using selector patterns.
TL;DR
- Prevent cascading consumer re-renders by splitting combined
contextobjects. - Separate volatile data state from invariant
dispatchaction functions. - Memoize Provider value props with
useMemoto preserve referential equality.
Value Object Memoization
Unmemoized Provider BugCreating new object in provider value triggers renders on every tick.
// BAD: New object on every render
<Context.Provider value={{ count, inc }}>Memoized Provider ValueWrap value in useMemo so consumers only re-render on changes.
const value = useMemo(() => ({
count,
inc,
}), [count, inc]);
return (
<Context.Provider value={value}>
{children}
</Context.Provider>
);Callback StabilizationWrap updater methods in useCallback before passing to context.
const inc = useCallback(() => {
setCount((c) => c + 1);
}, []);Context Splitting Pattern
Split State and DispatchDispatch context remains completely stable across all state changes.
const StateCtx = createContext(initial);
const DispatchCtx = createContext(noop);
function Provider({ children }) {
const [state, dispatch] =
useReducer(reducer, initial);
return (
<DispatchCtx.Provider value={dispatch}>
<StateCtx.Provider value={state}>
{children}
</StateCtx.Provider>
</DispatchCtx.Provider>
);
}Action-Only ConsumerComponent consuming only dispatch never re-renders on state.
export function AddButton() {
const dispatch = useContext(DispatchCtx);
const add = () => dispatch({ type: 'add' });
return <Button onClick={add}>Add</Button>;
}Data-Only ConsumerComponent reads state without subscribing to action triggers.
export function CounterBadge() {
const state = useContext(StateCtx);
return <Badge text={state.count} />;
}Domain Segregation
Frequency SeparationIsolate rapid updates from static configuration values.
// Fast changes: separate context
const MousePositionCtx = createContext({ x: 0, y: 0 });
// Slow changes: separate context
const UserProfileCtx = createContext(user);Subtree LocalizationMount providers as close to consumers as possible.
// Mount provider only around the relevant table widget
<TableProvider>
<DataTable />
</TableProvider>Intermediary Memo ComponentExtract children into memoized subcomponents below context.
function Parent() {
const data = useContext(MyCtx);
return <MemoChild data={data.specificField} />;
}High-Frequency Alternatives
useSyncExternalStoreSubscribe to external store with fine-grained selectors.
import { useSyncExternalStore } from 'react';
const val = useSyncExternalStore(
store.sub,
store.getSnapshot
);External Store LibrariesZustand provides automatic selector-based subscriptions.
const count = useStore((state) => state.count);Context Evaluation RuleUse Context for theme and auth; use stores for large data.
// Theme, Auth, Config -> Context
// Tables, Carts, Streams -> External StoresTips
- Split frequently changing state from rarely changing state into separate
contextproviders to isolate re-render cascades. - Always wrap context provider value objects in
useMemoso consumers only re-render when properties actually change.
Warnings
- React
Contextis designed for low-frequency updates; high-frequency data like animation coordinates will degrade performance. - Remember that
React.memocannot stop a component from re-rendering if that component callsuseContexton an updated context.
In Practice
Implement separate state and dispatch contexts to prevent button re-renders when count changes.
- Create independent StateContext and DispatchContext instances.
- Manage state transitions using useReducer inside the provider.
- Expose separate useCountState and useCountDispatch hooks.
- Verify that action trigger buttons remain immune to state re-renders.
type Fn = () => void;
const StateCtx = createContext(0);
const DispatchCtx = createContext<Fn>(() => {});
export function Provider({ children }: any) {
const [count, setCount] = useState(0);
const inc = useCallback(() => {
setCount((c) => c + 1);
}, []);
return (
<DispatchCtx.Provider value={inc}>
<StateCtx.Provider value={count}>
{children}
</StateCtx.Provider>
</DispatchCtx.Provider>
);
}
export function ActionBtn() {
const inc = useContext(DispatchCtx);
return <Button onClick={inc}>Add</Button>;
}FAQ
React.memo only skips re-renders when a component props remain unchanged. If a component calls useContext, any update to that context bypasses prop comparison and forces a re-render.
By creating one context for state data and a separate context for the dispatch updater function, components that only trigger actions never re-render when state data updates.
Migrate to external state management when you require fine-grained property subscriptions, high-frequency updates, or when context splitting becomes too cumbersome to maintain.