Lesson 48 of 80
Knowing that every consumer re-renders on any value change is only half the lesson — these are the concrete patterns for actually engineering around it.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Every component that calls useContext for a given context re-renders whenever that context's value changes — all of them, regardless of which part of the value they actually read. That fact alone isn't news if you've already covered Context's basics. What's less obvious is what you actually do about it in real code, beyond "memoize the value object." There are two concrete, commonly-used patterns worth knowing by name: splitting a context into pieces that change at different rates, and using the children prop to stop a re-render from cascading downward at all.
A useReducer's dispatch function has a stable reference across renders — it never changes. That makes it a natural candidate for its own context, separate from the state it operates on:
const CountStateContext = createContext(0);
const CountDispatchContext = createContext(() => {});
function CountProvider({ children }) {
const [count, dispatch] = useReducer(
(state, action) => (action === "inc" ? state + 1 : state),
0
);
return (
<CountDispatchContext.Provider value={dispatch}>
<CountStateContext.Provider value={count}>
{children}
</CountStateContext.Provider>
</CountDispatchContext.Provider>
);
}
function IncrementButton() {
const dispatch = useContext(CountDispatchContext);
return <button onClick={() => dispatch("inc")}>+1</button>;
// never re-renders on count changes — dispatch's reference never changes
}
IncrementButton only reads CountDispatchContext. Since dispatch is referentially stable across every render of CountProvider, that context's value never "changes" as far as React can tell — so IncrementButton never re-renders when count updates, even though it lives inside the same provider tree.
The second pattern is passing children through instead of rendering JSX inline inside the component that owns the changing state:
function CountProvider({ children }) {
const [count, setCount] = useState(0);
return <CountContext.Provider value={count}>{children}</CountContext.Provider>;
}
If CountProvider itself re-renders for a reason unrelated to count (its parent re-rendered, say), children here is still the exact same element reference it was handed — so React can bail out of re-rendering that subtree entirely, independent of whatever triggered CountProvider's own re-render.
CountStateContext and CountDispatchContext still re-renders on every count change — splitting doesn't create granularity that doesn't exist in what the component actually consumes.children-as-prop trick doesn't touch the "context value changed" case at all. It's easy to reach for it expecting it to also stop consumer re-renders when the value itself updates — it won't, because consumers subscribe directly to the context, not to whether their parent re-rendered.use-context-selector) exist for the case these patterns don't cover: genuinely granular "only re-render if this specific field changed" subscriptions. They're a real tradeoff against Context's simplicity, worth reaching for once splitting and memoizing stop being enough — not a day-one default.value on every render defeats splitting and the children trick alike, since the context "changes" on every single render regardless of what pattern sits around it.This builds directly on the earlier context lesson's core point — every consumer re-renders on any value change, not just the consumers that use the part that changed. The patterns here (separating what changes from what doesn't, and letting children carry a stable reference past a re-render) are how that constraint actually gets worked around in real code, not a different mechanism.