Context Performance & Splitting
Understand why context re-renders consumers, and split it so fast-changing data does not drag the tree down.
Context is a delivery mechanism, not a performance feature. When a Provider's value changes by identity, every consumer of that context re-renders β even ones that only read a field that did not change. For low-frequency data (theme, locale, current user) this is fine. For anything that updates on every keystroke or animation frame, it can quietly cost you.
The two classic mistakes
- A fresh object each render.
value={{ user, setUser }}creates a new object every time the Provider renders, so consumers re-render even when nothing meaningful changed. Memoize it. - One mega-context. Bundling state and dispatch together means a component that only dispatches still re-renders when state changes.
Split state from dispatch
<StateCtx.Provider value={state}>
<DispatchCtx.Provider value={dispatch}>
{children}
</DispatchCtx.Provider>
</StateCtx.Provider>Now a button that only needs dispatch reads DispatchCtx. Since dispatch is stable, that button never re-renders from state changes. Only components reading StateCtx update when state moves.
When splitting is not enough
If many components each need a different slice of a fast-changing store, context is the wrong tool β reach for useSyncExternalStore or a store library (Zustand, Redux Toolkit) that lets each component subscribe to just its slice with a selector.
Example
import { createContext, useContext, useReducer, useMemo } from 'react';
const CartStateContext = createContext(null);
const CartDispatchContext = createContext(null);
function cartReducer(state, action) {
switch (action.type) {
case 'add':
return { ...state, count: state.count + 1 };
case 'clear':
return { ...state, count: 0 };
default:
return state;
}
}
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { count: 0 });
// state is a new object only when it truly changes;
// dispatch is guaranteed stable by React.
const stateValue = useMemo(() => state, [state]);
return (
<CartStateContext.Provider value={stateValue}>
<CartDispatchContext.Provider value={dispatch}>
{children}
</CartDispatchContext.Provider>
</CartStateContext.Provider>
);
}
// Reads dispatch only -> never re-renders when count changes.
function AddButton() {
const dispatch = useContext(CartDispatchContext);
return <button onClick={() => dispatch({ type: 'add' })}>Add to cart</button>;
}
// Reads state -> re-renders when count changes (correctly).
function CartBadge() {
const { count } = useContext(CartStateContext);
return <span>{count}</span>;
}When to use it
- A design system splits theme tokens and the active-theme setter into two contexts so components that only read tokens do not re-render when the setter reference changes.
- A collaboration tool moves cursor position out of the user context into a separate CursorContext so the 60 fps cursor updates do not re-render the entire authenticated-user subtree.
- A dashboard separates slow-changing user preferences from fast-changing notification counts into two contexts so the preferences consumers never re-render during notification polling.
More examples
Split state and dispatch contexts
Separates state and dispatch into two contexts so components that only dispatch do not re-render on state changes.
import { createContext, useContext, useReducer } from 'react';
const StateCtx = createContext(null);
const DispatchCtx = createContext(null);
export function StoreProvider({ reducer, initial, children }) {
const [state, dispatch] = useReducer(reducer, initial);
return (
<DispatchCtx.Provider value={dispatch}>
<StateCtx.Provider value={state}>
{children}
</StateCtx.Provider>
</DispatchCtx.Provider>
);
}
export const useStore = () => useContext(StateCtx);
export const useDispatch = () => useContext(DispatchCtx);Stable value with useMemo
Wraps the context value in useMemo so a new object is only created when user changes, not on every parent render.
import { createContext, useContext, useState, useMemo } from 'react';
const AuthCtx = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const value = useMemo(
() => ({ user, login: setUser, logout: () => setUser(null) }),
[user]
);
return <AuthCtx.Provider value={value}>{children}</AuthCtx.Provider>;
}
export const useAuth = () => useContext(AuthCtx);Select a context slice to avoid re-renders
Destructures only the needed field from the context value; pair with use-context-selector to prevent non-field re-renders.
import { createContext, useContext } from 'react';
const SettingsCtx = createContext({ theme: 'light', lang: 'en', fontSize: 14 });
// Only re-renders when theme changes, not when lang or fontSize change
function ThemeAwareButton({ children }) {
const { theme } = useContext(SettingsCtx);
return <button className={`btn btn--${theme}`}>{children}</button>;
}
// For more granular control, use a selector library like use-context-selector
Discussion