React2 min read

One context is re-rendering your whole app

Context has no selectors. Every consumer re-renders when the value changes, even the ones using a field that did not. Splitting the context fixes most of it.

You put the user, the theme, the cart, and a few setters into one AppContext. Months later, opening the cart drawer re-renders every screen in the app.

Context is a delivery mechanism, not a store. It has one rule and no way around it.

The mental model

Every component that calls useContext(X) re-renders when X's value changes. There is no partial subscription, no "only when cart changes". The comparison is on the whole value, with Object.is.

So this re-renders every consumer on every render of the provider:

<AppContext.Provider value={{ user, theme, cart, setCart }}>

The object literal is new each time, so even a component that reads only theme re-renders when the cart changes.

Fix 1: memoise the value

The smallest step, and necessary but not sufficient:

const value = useMemo(
  () => ({ user, theme, cart, setCart }),
  [user, theme, cart, setCart],
);

Now the provider re-rendering for an unrelated reason does not push a new object. But when cart genuinely changes, everyone still re-renders.

Fix 2: split by change frequency

This is the real fix. Group values by how often they change, not by what feature they belong to.

<ThemeContext.Provider value={theme}>
  <UserContext.Provider value={user}>
    <CartContext.Provider value={cart}>
      {children}
    </CartContext.Provider>
  </UserContext.Provider>
</ThemeContext.Provider>

The theme changes twice a day. The cart changes constantly. They have no business sharing a subscription.

Fix 3: split the value from the setters

Setters from useState and dispatch from useReducer are stable for the life of the component. Put them in their own context and a component that only writes will never re-render:

const CartContext = createContext(null);
const CartDispatchContext = createContext(null);

function CartProvider({ children }) {
  const [cart, dispatch] = useReducer(cartReducer, initialCart);

  return (
    <CartContext.Provider value={cart}>
      <CartDispatchContext.Provider value={dispatch}>
        {children}
      </CartDispatchContext.Provider>
    </CartContext.Provider>
  );
}

export const useCart = () => useContext(CartContext);
export const useCartDispatch = () => useContext(CartDispatchContext);

An "Add to cart" button calls useCartDispatch() only. It never re-renders when the cart changes, which is right: it does not display anything from the cart.

No useMemo needed either. cart and dispatch are already stable references.

Fix 4: keep the provider from re-rendering

If the provider component itself re-renders for its own reasons, take children as a prop so the tree below is not rebuilt:

function App() {
  return (
    <CartProvider>
      <Routes />   {/* created by App, not by CartProvider */}
    </CartProvider>
  );
}

When to stop and use a store

Context is good for values that are read widely and written rarely: theme, locale, the signed in user, a feature flag set.

For state that changes often and is read by many components in different slices, a store with selector based subscriptions is a better fit. Zustand, Jotai, or Redux Toolkit all let a component subscribe to one field and skip renders when the rest changes. That is the thing context cannot do.

The signal that you have crossed the line: you are memoising context values, splitting providers three ways, and still watching unrelated components render in the profiler.

Written by Rahul Mourya, full stack engineer. Spotted a mistake or want the follow-up post? Email me.

All posts
  • React2 min read

    You pushed to the array and nothing rendered

    React compares state with Object.is, so changing an array or object in place looks like no change at all. Here are the copy patterns, including the one for nested data.

    • state
    • immutability
    • rendering
  • React3 min read

    Your useCallback is doing nothing without memo

    Wrapping every function in useCallback does not stop re-renders. It only helps when the child is memoised and every other prop is stable. Here is how to tell, and what to do instead.

    • performance
    • hooks
    • memoization