How to Stop Context Re-Render Cascades
This guide is part of React Rendering Performance, within Framework Performance. React context is a convenient way to share values without passing props through every level. Its performance behaviour is simple and often surprising: when a provider's value changes (by identity), every component that calls useContext for that context re-renders, regardless of which part of the value it reads. A single "app state" context holding the user, theme, cart, filters and UI flags means that toggling a sidebar re-renders every component that reads the cart.
Worse, a provider whose value is an object literal (value={{ user, setUser }}) creates a new value on every render of the provider, so all consumers re-render whenever the provider's parent renders, even if nothing changed. These cascades are a frequent cause of slow interactions in medium and large React apps.
Rapid Diagnosis
- Profile an interaction and check "Why did this render?" for "Context changed" on many components.
- Look at provider values: inline objects or arrays are recreated each render.
- List what each context holds: unrelated values in one context are a red flag.
- Check update frequency: values that change often (input text, hover state, timers) in a context reach many consumers.
Root Cause Analysis
1. Unmemoized provider values. A new object every render.
2. Mixed concerns. Rarely and frequently changing values in one context.
3. State and setters together. Components that only dispatch actions re-render when state changes.
4. Hot state in context. Rapidly changing values broadcast to many consumers.
Step-by-Step Resolution
1. Memoize provider values
function CartProvider({ children }) {
const [items, setItems] = useState([]);
const value = useMemo(() => ({ items, setItems }), [items]); // stable unless items change
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}
// trade-off: useMemo here is cheap and prevents re-rendering every consumer when
// the provider's parent re-renders for unrelated reasons.
2. Split contexts by concern and change frequency
Separate user, theme, cart and UI state into their own contexts. Components subscribe only to what they need.
3. Separate state from actions
const CartStateContext = createContext(null);
const CartActionsContext = createContext(null);
function CartProvider({ children }) {
const [items, dispatch] = useReducer(cartReducer, []);
return (
<CartActionsContext.Provider value={dispatch}> {/* dispatch is stable */}
<CartStateContext.Provider value={items}>{children}</CartStateContext.Provider>
</CartActionsContext.Provider>
);
}
// "Add to cart" buttons use only CartActionsContext and never re-render on cart changes.
4. Use a selector-based store for hot state
For frequently changing shared state, use a store with selectors (Zustand, Redux with useSelector, Jotai atoms, or React's useSyncExternalStore), so components re-render only when the slice they select changes.
Verification
Profile the same interactions: "Context changed" should appear only on components that use the changed value. Count components rendered per interaction before and after. In the field, check INP for interactions that previously caused cascades.
Worked Example: An E-commerce Header
An e-commerce site used one StoreContext with user, cart, wishlist, currency, search query and a megaMenuOpen flag. Typing in the header search box updated searchQuery in context, re-rendering 380 consumers — product cards (which read currency), wishlist buttons and cart badges — on every keystroke; INP for typing was 280ms. The team moved the search query into the search component's local state, split the context into user, cart, wishlist and preferences contexts, separated cart state from actions, and memoized provider values. Keystrokes now re-rendered only the search box, and INP for typing fell to 50ms.
Context Is Not a State Manager
Context is a dependency injection mechanism: it passes a value down the tree. It does not offer selectors, batching across contexts, or fine-grained subscriptions. For slowly changing values (theme, locale, authenticated user, feature flags), context is ideal. For rapidly changing or large shared state, a store with subscriptions scales better, because components subscribe to exactly the data they need. Many apps combine both: context provides the store instance, and components use selector hooks to read from it.
Migrating an Existing App Context
Large app-wide contexts are common in mature codebases, and splitting them can touch many files. Migrate incrementally. First, memoize the provider value — a one-line change that often removes the worst cascades. Next, move fast-changing values (input text, hover and drag state) out of context into local state or a selector store. Then split the remaining context by concern, starting with the values whose changes cause the largest re-renders in the Profiler. Keep a temporary compatibility hook that combines the new contexts for components not yet migrated, and remove it once all consumers use the focused hooks. Measure after each step so the team can see which change mattered most.
Common Mistakes
- Inline provider values. Re-render all consumers on every provider render.
- One context for everything. Unrelated changes re-render unrelated components.
- Putting input text in global context. Every keystroke reaches every consumer.
- Assuming React.memo stops context renders. Memoized components still re-render when a consumed context changes.
Edge Cases
Context with the React Compiler. The compiler memoizes provider values and consumers' output, but consumers still re-render when the context value changes.
Server Components. Context is only available in client components; keep providers as low in the tree as possible.
Default values. Components outside a provider get the default value; avoid relying on that for performance.
Nested providers. Nearest provider wins; splitting can create deep provider trees, which is fine.
FAQ
Why does every context consumer re-render?
React compares the provider value by identity. When it changes, all consumers of that context re-render, regardless of which properties they read.
Does React.memo prevent context re-renders?
No. Memoization skips renders caused by parent props, but a component consuming a changed context always re-renders.
How many contexts is too many?
There is no practical limit. Many small, focused contexts are better for performance than one large one.
Should I use Redux or Zustand instead of context?
For frequently updated shared state, selector-based stores avoid broad re-renders. For rarely changing values, context is fine.
Is there a context selector in React?
Not in stable React. Libraries such as use-context-selector provide the pattern, or use useSyncExternalStore with your own store.
Does splitting state and dispatch help?
Yes. Components that only trigger actions subscribe to the stable dispatch context and never re-render on state changes.
How do I find which context caused a render?
The React Profiler's "Why did this render?" shows "Context changed"; the Components panel shows which contexts a component consumes.
Can context cause performance problems in small apps?
Rarely. The cost scales with the number of consumers and update frequency; it becomes noticeable in larger apps or with frequently changing values.
Does useSyncExternalStore help?
Yes. It lets components subscribe to an external store with a selector, re-rendering only when the selected value changes, and works correctly with concurrent rendering.
Can I put a store instance in context?
Yes, and it is a common pattern: the context value (the store) never changes, and components read slices from it with selector hooks.
Do Server Components use context?
No. Context only works in client components, so providers should wrap only the client parts of the tree that need them.
Should theme switching use context?
Yes. Theme changes are rare, so broad re-renders on change are acceptable; CSS variables can avoid re-rendering entirely.
Related
- When useMemo and useCallback actually help — memoizing provider values.
- Finding wasted renders with the React Profiler — spotting context renders.
- useTransition and useDeferredValue for INP — keeping updates responsive.