skip to content

A React context holds an object with several unrelated fields, and a component that reads only one of them re-renders whenever any field changes. Why can't the component subscribe to just that field, and what do you do about it?

level: seniorimportance: should knowfreq 46%

answer

  1. no selector argument on the read
  2. one value, one comparison, all readers
  3. split by how often it changes
  4. memo cannot block a context read
  5. god-context is the smell

basics

~20 s

Context has no selector API: React compares the whole value with Object.is and notifies every reader, with no way to opt out per field. The structural fix is to split the value into several contexts along how often each part changes.

solid answer

~50 s

`useContext` takes a context and returns its value — there is no second argument for a selector and no equality-function parameter, so React cannot know that a component only cares about one field. The comparison is a single `Object.is` on the value, and the notification is all-or-nothing for every reader below the provider. Adding `React.memo` to the consumer does not help either, because a context read is not a prop. So you fix it structurally: split the value into separate contexts grouped by update frequency, so a fast-changing field lives in its own context and only its genuine readers pay for it. A common split is one context per concern — auth, theme, the volatile piece — rather than one god-context. When the value is large and truly needs fine-grained reads, that is the signal that a subscribable store, not context, is the right tool.

go deeper

for a junior

Know that reading a context gives you the whole value, and that there is no way to ask for just one field of it.

for a middle

Explain that React compares the value once with Object.is and notifies every reader, and show the fix of splitting one context into several.

for a senior

Order the fixes by leverage — split by update frequency, separate read from write, question whether volatile state should be shared at all — and back the choice with Profiler evidence.

for a principal

Own the boundary: decide at what point a growing set of providers is a signal that the app needs fine-grained subscription instead, and make that call once for the codebase rather than per feature.

## Why there is no selector The `useContext` API is deliberately minimal: you pass the context object, you get its current value. There is no selector argument, no comparison function, no `useContextSelector`. React 19's `use(Context)` is the same read with different call-site rules — it may appear inside conditionals and loops — not a finer-grained subscription. That is a consequence of how context propagation works. React holds one value per provider and compares it as a single unit with `Object.is`. When that comparison fails, it schedules work on every fiber below that provider that reads that context. There is no per-reader bookkeeping about *which part* of the value each one touched, because the read is just a function call returning the whole value — by the time your component destructures one field, React is long done. So a context is closer to dependency injection with change notification than to a store. It broadcasts "this value is different now" and every subscriber wakes up. ## The failure mode this produces The damage scales with how unrelated the bundled fields are: ```jsx const value = useMemo( () => ({ user, theme, sidebarOpen, cursor }), [user, theme, sidebarOpen, cursor] ); ``` This is *correctly memoized* — and still terrible, because `cursor` updates on every mouse move, so `user` and `theme` readers across the entire app re-render at pointer frequency. Memoization solved the identity problem and left the granularity problem completely untouched. Candidates who have only learned "wrap it in `useMemo`" get stuck exactly here. ## The structural fixes, in the order you should try them **1. Split by update frequency, not by data model tidiness.** This is the highest-leverage change. Group fields by how often they change and give each group its own context and provider: ```jsx <AuthContext value={user}> <ThemeContext value={theme}> <CursorContext value={cursor}>{children}</CursorContext> </ThemeContext> </AuthContext> ``` Now a mouse move notifies only the components that read `CursorContext`. The rule of thumb: a field that changes orders of magnitude more often than its neighbours belongs alone. **2. Split read from write.** The special case of the above: components that only invoke an updater should read a context whose value is a stable `dispatch` or setter, so they are never notified at all. **3. Ask whether the volatile state belongs in context at all.** A cursor position, a scroll offset, a drag delta, an in-progress text input — these frequently do not need to be shared globally. Colocating them with the one component that uses them removes the whole problem rather than making it cheaper. **4. Narrow the subtree.** A provider does not have to sit at the root. Rendering it around only the branch that needs it caps how many components can be notified in the first place. ## What does not work, and why - **`React.memo` on the consumer.** It compares props; a context read is not a prop. The consumer re-renders anyway. - **A custom hook that destructures one field.** `const { theme } = useContext(AppContext)` runs *after* the component was already scheduled and rendered. The re-render already happened; you are only choosing what to do with the value. - **A deeper equality check inside the provider.** You can stop the *value* from changing identity when nothing changed, but you cannot make it change for some readers and not others. - **`useMemo` around the consumer's expensive work.** This is not useless — it can make the wasted render cheap — but the render still happens, and every child element is still re-created. ## The honest limit When a shared value is genuinely large, genuinely read field-by-field by many components, and genuinely updates often, splitting contexts stops scaling: you end up with a dozen providers and a wrapper pyramid, and you still cannot express "notify me when `items[42].done` flips". That is context telling you it is the wrong shape for the job. The React primitive that exists for fine-grained subscription is `useSyncExternalStore`, and store libraries are built on exactly that — but the choice of whether to adopt one, and which, is a separate decision from the context-level fixes above. ## Answering it in an interview Name the constraint first: context has no selector because the value is compared and broadcast as one unit. Then give the fix in the right order — split by update frequency, split read from write, question whether the volatile state should be shared at all — and finish by naming the boundary where context stops being the right tool. That progression shows you understand the mechanism rather than reciting a workaround.

  • A team memoized the provider value carefully and re-renders are still excessive. What does that tell you?
    That they fixed identity churn, not granularity. Memoization stops the value changing when nothing changed; it does nothing about a value that changes for one real reason and wakes every reader regardless. Look at what inside that object changes most often and pull it into its own context — or out of context entirely.
  • Where would you place a provider that carries fast-changing state, and why does placement matter?
    As low in the tree as the state's actual readers allow. A provider at the root makes every consumer in the app a candidate for notification; wrapping only the branch that needs it bounds the blast radius structurally, before any memoization is involved. Placement is the cheapest optimisation available and costs no runtime machinery.
  • Does destructuring a single field in a custom hook that wraps useContext reduce re-renders?
    No. The component was already scheduled and re-rendered before any destructuring runs — the hook just decides what to hand back. It is good ergonomics and gives you one place to add a missing-provider check, but it has zero effect on the render profile.
  • How many contexts is too many?
    There is no fixed number, but the smell is a wrapper pyramid of providers whose splits no longer map to anything a reader would recognise — splitting purely to dodge notifications rather than by concern. At that point the requirement is fine-grained subscription, which is a different tool's job, not more contexts.

saying these in an interview costs you the question

  • Claims useContext takes a selector or equality function
  • Thinks React.memo on the consumer filters context updates
  • Believes destructuring one field limits what triggers a re-render
  • Says useMemo on the value solves per-field granularity
  • Puts every piece of app state into one root-level context

context