Context Performance Pitfalls: Why Every Consumer Re-renders and How to Fix It
Every component that calls useContext re-renders whenever the context value changes — even if it only uses one small slice of that value. This is the most common context performance bug, and there are three patterns to fix it.
Every component that calls useContext re-renders whenever the context value changes. Not just the parts of the UI that use the changed slice — every consumer, regardless of what it actually reads.
This is fine for infrequently changing data like theme or auth status. It becomes a problem when the context holds multiple values that change at different rates, or when many components subscribe to the same context.
const AppContext = createContext(null);
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [cart, setCart] = useState([]);
return (
<AppContext.Provider value={{ user, setUser, theme, setTheme, cart, setCart }}>
{/* ... */}
</AppContext.Provider>
);
}A component that only needs theme will re-render whenever cart changes. It does not use cart. It does not display anything based on cart. But it re-renders anyway, because the context value object is a new reference on every render.
Context does not do granular subscriptions. It is all-or-nothing.
Fix 1: Split Contexts by Concern
The most effective fix is splitting one large context into multiple smaller ones, each holding a distinct concern:
const UserContext = createContext(null);
const ThemeContext = createContext(null);
const CartContext = createContext(null);
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [cart, setCart] = useState([]);
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<CartContext.Provider value={{ cart, setCart }}>
{/* ... */}
</CartContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}Now a component that calls useContext(ThemeContext) only re-renders when theme changes. Cart updates are invisible to it.
Fix 2: Memoize the Context Value
If splitting is not practical, stabilize the value object with useMemo so it only changes when the underlying data actually changes:
const value = useMemo(
() => ({ user, setUser, theme, setTheme }),
[user, theme]
);
<AppContext.Provider value={value}>Without useMemo, the value object is a new reference on every render — even if user and theme did not change — causing all consumers to re-render. useMemo ensures the reference is stable when the data is stable.
Fix 3: Reach for a State Management Library
Context is the right starting point. But when you have frequently changing state shared across many components, tools built for exactly this problem are worth the investment:
Zustand — a minimal store with selector-based subscriptions. Components only re-render when the slice they subscribe to changes.
Redux Toolkit — the same selector model with more structure, dev tooling, and middleware support.
TanStack Query — specifically for server state: fetching, caching, background refetching, and synchronization with no manual useEffect required.
The distinction that matters: use context for static or infrequently changing global state (theme, locale, auth). For frequently updating data shared across many components, a library with granular subscriptions is the right tool.
When Context Is the Right Tool
Context is not a bad tool — it is a misapplied one when used for the wrong job.
Use it for global configuration that most of the app reads but rarely changes: whether the user is authenticated, the current theme, the active locale. Components that consume this data rarely re-render because of it, so the all-or-nothing update model does not hurt.
Do not use it as a substitute for a proper state management solution in performance-critical or frequently-updating scenarios. That is where context's simplicity becomes its limitation.
Practice what you just read.
Keep reading