Fixing a React Context Provider That Re-renders Every Consumer on Every Update
A single unrelated piece of state changes — a modal opens, a tooltip's position updates — and dozens of components scattered across the app re-render, even ones that only read a completely different field from the same context. React Profiler shows the cascade clearly; what it doesn't show at a glance is that this is how React Context is designed to work, not a bug in any individual component.
The Problem
An app uses React Context to share state across a large part of the tree — user session data, theme settings, a shopping cart, UI state like which modal is open. A single field in that context updates, and the React Profiler shows dozens of components re-rendering as a result — including ones that never read the specific field that actually changed, only some other, unrelated piece of the same context object. Performance degrades noticeably as the app grows, particularly on interactions that update frequently (scroll position, form input, a live counter) if any of that state happens to live in a widely-consumed context.
Why It Happens
useContext subscribes a component to the entire context value, not to the specific fields it actually reads
When a component calls useContext(MyContext), React re-renders that component whenever the context's Provider passes a new value — full stop, regardless of which specific properties of that value the component actually destructures and uses in its render. Reading only user.name from a context object that also contains cart, theme, and modalState still means the component re-renders when cart changes, even though nothing about what it renders depends on the cart.
A Provider's value prop being a new object literal on every render defeats even React.memo on children
A common pattern — <MyContext.Provider value={{ user, cart, theme }}> — creates a brand-new object on every render of the component that owns the Provider, regardless of whether user, cart, or theme individually changed. Because the object reference itself is different every time, every consumer re-renders on every Provider re-render, independent of the specific field-level changes that occurred.
A single large context bundles unrelated pieces of state that have very different update frequencies
Grouping frequently-changing state (mouse position, a live search query) together with rarely-changing state (user profile, feature flags) into one context means the rarely-changing consumers pay the re-render cost of the frequently-changing state's every update, purely because they happen to share a context object rather than because they have any actual coupling.
This is easy to miss because each individual component's render is cheap — the problem is the aggregate cost across many components
A single re-render of a small presentational component might take under a millisecond, which doesn't look alarming in isolation. The actual cost is dozens or hundreds of these cheap re-renders happening simultaneously on every context update, which is easy to overlook unless profiled across the whole tree rather than one component at a time.
The Fix
1. Split one large context into multiple smaller, independently-updating contexts
// Instead of one AppContext with everything:
const UserContext = createContext(null);
const CartContext = createContext(null);
const ThemeContext = createContext(null);
function AppProviders({ children }) {
return (
{children}
);
}
Splitting context by how frequently and independently each piece of state actually changes means a component subscribed only to ThemeContext no longer re-renders when cart updates — each context's consumers only pay the re-render cost of the specific state they actually depend on.
2. Memoize the Provider's value to avoid a new object reference on every render
function CartProvider({ children }) {
const [items, setItems] = useState([]);
const value = useMemo(() => ({ items, setItems }), [items]);
return {children} ;
}
Wrapping the Provider's value in useMemo, keyed on the actual state it depends on, ensures the value reference only changes when the underlying data genuinely changes — removing the re-renders caused purely by a fresh object literal being created on every parent render regardless of whether anything meaningful changed.
3. Use a selector pattern or a state library that supports fine-grained subscriptions for frequently-changing, widely-consumed state
import { create } from "zustand";
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
}));
// A component reading only itemCount only re-renders when itemCount changes,
// not on every unrelated cart field update
function CartBadge() {
const itemCount = useCartStore((state) => state.items.length);
return {itemCount};
}
For state that's both frequently updated and consumed in many places, a library offering selector-based subscriptions (Zustand, Jotai, Redux with reselect) lets each consumer subscribe to exactly the slice of state it reads, sidestepping Context's inherent all-or-nothing subscription model entirely for the cases where it matters most.
4. Push state down closer to where it's actually used instead of lifting it to a shared context by default
// If tooltip position only matters to the tooltip itself,
// it doesn't belong in shared app-wide context at all
function Tooltip() {
const [position, setPosition] = useState({ x: 0, y: 0 }); // local, not global
// ...
}
Not every piece of state needs to live in a shared context in the first place — state that's genuinely local to one component or a small subtree should stay local, which removes it from the shared re-render cost entirely rather than needing any of the above techniques to manage it.
Why This Works
Each fix addresses a different source of unnecessary re-renders stemming from Context's all-or-nothing subscription model. Splitting contexts by update frequency and ownership ensures consumers only re-render for state they actually depend on; memoizing the Provider value removes re-renders caused by object identity churn rather than real data changes; a selector-based state library sidesteps the coarse subscription model entirely for state that genuinely needs fine-grained reactivity; and keeping local state local removes it from the shared re-render surface in the first place.
Conclusion
Widespread re-rendering from a Context update isn't a bug in any individual consumer — it's Context's subscription model working exactly as designed: subscribe to the whole value, re-render on any change to it. Split large contexts by how independently their pieces of state actually change, memoize the Provider's value to avoid unnecessary object identity churn, reach for a selector-based state library when frequently-changing state is widely consumed, and keep state that's genuinely local out of shared context in the first place.
