Back to React
React
medium
mid

When does React Context cause re renders, and how do you avoid them?

Every consumer of a context re-renders whenever the provider's `value` changes by reference. Stabilize the value, split contexts, or use a selector library (Zustand, use-context-selector) for high-churn state.

6 min read·~12 min to think through

Context is React's built-in dependency injection. It's perfect for low-frequency values — theme, locale, current user, feature flags. It's wrong for high-frequency state — mouse position, form fields, anything that changes every keystroke — because every consumer re-renders on every value change.

The mechanism: <Provider value={…}> triggers a re-render in every component that calls useContext, regardless of which part of the value they read. There's no built-in selector.

Three fixes, in increasing power:

  1. Stabilize the value. Wrap in useMemo so the reference only changes when underlying data does. Catches the most common bug — inline object literals creating a new reference every render.
  2. Split contexts. Separate UserContext from UserActionsContext so a re-render of the user object doesn't re-render every component that only needs logout(). Actions barely change; data does.
  3. Selector pattern. use-context-selector (or graduate to Zustand/Redux) lets consumers subscribe to a specific slice. Only components reading that slice re-render. This is what state libraries solve out of the box.

Default: use Context for things that rarely change. Reach for a real store the moment you find yourself memoizing aggressively or splitting contexts to avoid render storms — that's the signal you've outgrown raw Context.

Code

tsx
<UserContext.Provider value={{ user, logout }}>  // ⚠️ new object each render
  <App />
</UserContext.Provider>
Wrong — new value reference every render
tsx
const StateCtx = createContext<User | null>(null);
const ActionsCtx = createContext<{ logout: () => void } | null>(null);

function UserProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const actions = useMemo(() => ({ logout: () => setUser(null) }), []);
  return (
    <StateCtx.Provider value={user}>
      <ActionsCtx.Provider value={actions}>{children}</ActionsCtx.Provider>
    </StateCtx.Provider>
  );
}
Right — split contexts and stabilize value

Follow-up questions

  • How does use-context-selector work under the hood?
  • Why is Zustand cheaper than Context for high-churn state?
  • When would you NOT split contexts?

Common mistakes

  • Inline `value={{ ... }}` literal that re-renders the whole subtree every parent render.
  • Stuffing every piece of app state into one giant Context.
  • Using Context for mouse/scroll position — should be a ref or external store.

Performance considerations

  • Selectors with strict equality (Zustand, Redux) avoid re-renders that Context can't.

Edge cases

  • Memoizing a context value with deps that miss a captured variable causes silent staleness.
  • Multiple providers stack — `useContext` reads the nearest provider above.

Real-world examples

  • Theme + locale + auth typically live in Context. Form state, query results, and live data go in a store or React Query.

Senior engineer discussion

Senior signal: explain why React doesn't ship a selector API, the bailout heuristics, and how the React Compiler relaxes some manual memoization needs.

Related questions