skip to content

A React codebase has accumulated around thirty Context providers, and a tech lead proposes consolidating client state into a single external store. How would you evaluate that proposal, and what would you measure before committing?

level: principalimportance: should knowfreq 32%

answer

  1. count is a smell, not a diagnosis
  2. sort into injection, server cache, client state
  3. measure update rate and slice interest
  4. custom hook is the migration seam
  5. one instance per request on the server

basics

~20 s

Separate the contexts by role first: most are usually dependency injection of values that never change and should stay. Measure which ones actually update at runtime, how often, how many consumers need slices, and how many non-React readers exist — then migrate only those.

solid answer

~50 s

Thirty providers is not itself a defect, so I would refuse to treat the count as the argument. First I classify: which contexts inject handles and near-constant configuration, which cache server data, and which hold genuinely shared client state that changes at runtime. Usually only a handful are in the third group, and only those are candidates. Then I gather evidence — update rate per context, consumer count and whether consumers need slices, profiler traces showing real render cost, and how many modules outside React read or write each value. Against that I weigh the cost: a second state system, a convention every team must learn, and the risk of a half-finished migration leaving two owners of the same state. If we go, we go incrementally behind the custom hooks components already call, one slice at a time, deleting each context only once nothing reads it.

go deeper

for a junior

Know that many providers is not automatically a problem, and that a value which never changes costs almost nothing to publish through Context. Ask what is actually slow before restructuring.

for a middle

Be ready to classify contexts by what they carry — injected handles, cached server data, live client state — and explain why only the third group is a candidate for a store.

for a senior

Bring evidence to the argument: update rates, consumer slice interest, profiler traces, out-of-tree readers, and a migration that hides behind existing custom hooks so call sites do not churn.

for a principal

Own it as an organisational decision: the store becomes a convention every team follows, so define what may live in it, set a stopping point, and account for per-request isolation on the server before endorsing the change.

## Reject the count as the argument "Thirty providers" is a smell someone noticed, not a diagnosis. Provider count correlates with how many independent concerns the app injects, which is often healthy. The proposal has to be justified by behaviour — what updates, how often, who reads it — not by the size of the tree in the React DevTools component panel. Start by saying that, then do the classification that turns the complaint into a decision. ## Sort the thirty into three populations **Dependency injection.** Theme, locale, feature flags, an API client, a logger, an analytics handle, a design-system configuration. These publish a value that is created once and never changes. Moving them into a store buys nothing and loses the ability to scope a different value to a subtree. Leave them alone. In most codebases this is the majority of the thirty. **Server data caches.** Contexts wrapping fetched data, hand-rolled loading and error flags, and a refetch function. Their real problem is staleness, deduplication and revalidation, which a client-state container does not solve either. Consolidating them into a store is motion without progress; they belong in a data-fetching layer. **Genuinely shared client state.** A cart, a multi-step wizard, editor selection state, a live collaboration presence list. This is the only population where the store question is real, and it is usually three to six of the thirty. The classification alone often ends the discussion, and it costs an afternoon. ## What I would measure - **Update rate per context.** Instrument or observe: how many times a session does each provider publish a new value? A context that updates once per login is not a candidate regardless of how many consumers it has. - **Consumer count and slice interest.** For each updating context, how many components read it, and do they use the whole value or one field? Broad consumption of one field is the specific shape selectors fix. - **Actual render cost.** Profiler evidence that the re-renders are expensive, not merely numerous. Thirty cheap re-renders of leaf components are not a user-visible problem, and the fix for expensive ones is often structural — move state down, pass expensive subtrees as children — rather than architectural. - **Out-of-tree readers.** Count the modules, interceptors and callbacks that need each value with no component on the stack. Every one of these is a hard argument for a store, because Context cannot serve them at all. - **Bug and incident history.** Which of these contexts have produced real defects — stale values, provider-ordering surprises, duplicated truth? Evidence beats aesthetics in this conversation. ## The costs to put on the other side of the ledger A store is a convention, and conventions are organisational commitments. Every future change starts with "does this belong in the store?", and the default answer drifts toward yes, which erodes colocation until components cannot be rendered in isolation or reused elsewhere. A partly-finished migration is worse than either endpoint, because the same concept ends up with two owners. And a module-level singleton store has a specific server-rendering hazard: one module instance shared across concurrent requests can leak one user's state into another's render, so a server-rendered app needs a per-request instance injected into the tree rather than a module global. ## How to migrate if the evidence supports it Use the custom hook as the seam. Components should already be calling `useCart()` rather than reading a context directly; if they are not, introduce that indirection first as a pure refactor. Then the implementation behind the hook changes from context to store with no call-site churn, one slice at a time, and each context is deleted only when nothing reads it. Keep Context in the picture for injecting the store instance so tests and per-request server renders can swap it. ## The answer that lands Say that you would agree to migrate the three or four contexts that show high update rates with slice-level consumption or out-of-tree readers, decline to migrate the injection contexts at all, route the server-data ones to a caching layer instead, and set a written rule for what is allowed in the store afterwards. That is a principal-level answer because it converts a stylistic complaint into a scoped, evidence-backed change with a stated stopping point.

  • Why is a half-finished migration worse than either endpoint?
    Because the same concept ends up with two owners, and readers must know which one is authoritative today. Updates get applied to one and read from the other, tests set up whichever the author remembered, and every new engineer asks the same question. If you cannot commit to finishing a slice, do not start it — migrate slice by slice so each step is complete.
  • What rule would you write down afterwards about what may live in the store?
    Roughly: state that several unrelated features both read and write, that changes at runtime, and that outlives any single subtree. Explicitly excluded are server data, form and wizard state scoped to one screen, and injected handles. Without a written boundary the store becomes the default home for everything and colocation disappears.
  • Why does the server-rendering hazard apply to a module singleton but not to Context?
    Because a module is instantiated once per process and shared by every concurrent render, so one request's writes are visible to another's. A context value is created during a render and scoped to that tree, so it is naturally per-request. The fix is to create the store per request and inject it through Context rather than importing a global.

saying these in an interview costs you the question

  • Thirty providers is obviously too many
  • Consolidate everything into one global store
  • Fewer providers always means simpler code
  • Migrate all contexts at once for consistency
  • A module singleton store is fine on the server

context