skip to content

You own a shared React module that other teams consume — say a <FeatureFlagProvider> plus hooks. What do you decide about its public surface: what to export, whether the Provider owns its own state, and whether two instances may coexist?

level: principalimportance: nice to knowfreq 24%

answer

  1. everything exported is a promise you keep
  2. private context means a changeable value shape
  3. uncontrolled by default, one documented escape hatch
  4. two mounts: intended scope or silent bug
  5. ship a test wrapper, not the context

basics

~20 s

Export the Provider and hooks, keep the context object private, and decide deliberately whether the Provider owns state or accepts it as a prop. Then state explicitly whether two mounted instances mean two independent scopes or a misconfiguration you should detect.

solid answer

~50 s

Three decisions. **Surface**: export the Provider and a small set of hooks; keep the context object internal, so the value shape stays yours to change and nobody can fabricate a value by mounting their own Provider. **State ownership**: a Provider that owns its state internally is the easiest to adopt — one tag and you are done — but consumers cannot control or seed it; an "uncontrolled with escape hatch" design, where it owns state by default but accepts an initial or fully-controlled value prop, covers both without two components. **Multiplicity**: decide whether two mounted Providers mean two legitimate scopes or a bug, and back that decision with something real — either document per-instance scoping, or detect a nested instance during render and warn. Ship the pieces adoption actually needs: a test wrapper, TypeScript types that are non-nullable after the hook narrows, and an error message that names the Provider.

go deeper

for a junior

Know that a context module normally exports a Provider and hooks, and that keeping the context object itself unexported is deliberate rather than an oversight.

for a middle

Explain how a narrow export surface keeps the value shape changeable, and what an uncontrolled-with-escape-hatch Provider gives consumers that a state-owning one does not.

for a senior

Show you would decide the failure modes up front: throwing hooks with named messages, a supported test wrapper, and detection for a mistakenly nested second instance.

for a principal

Treat every export as a versioned commitment across teams: argue what you refuse to expose, what a value-shape migration costs once exposed, and how duplicate package copies break context identity in consumers' builds.

## Why this is a design problem, not a coding one Once other teams depend on your module, everything you export is a contract you have to keep. Context modules are especially easy to over-expose because the pieces feel innocuous — a context object, a value type, a helper — and each one silently licenses a usage you will later want to forbid. ## Decision 1 — the export surface The defensible default is narrow: the Provider component, one or two hooks, and the types the hooks return. The context object stays private. Exporting the context object licenses two things you probably do not want. Consumers can mount their own Provider with a hand-made value, so your invariants (this value is always fully loaded, these fields are always consistent) stop holding anywhere in the app. And consumers can read it directly, which freezes the internal value shape: split one context into two later and you break them. But refusing to export it creates a real gap — tests, Storybook, and embedded previews genuinely need to inject values. Fill that gap on purpose rather than by leaking the context: ```jsx // Purpose-built, documented, and narrower than exposing the context export function FeatureFlagTestProvider({ flags, children }) { /* … */ } ``` A named test surface is better than an exported context, because you control its shape and can keep it honest as the internals move. ## Decision 2 — who owns the state There are three shapes, and the choice is a genuine tradeoff. **Provider owns everything.** It fetches or computes internally and exposes the result. Adoption is one line; nobody can get the wiring wrong. The costs are real, though: the consumer cannot seed it for tests or server-rendered data, cannot decide when the work happens, and cannot share the same data with non-React code. **Provider is a pure carrier.** It takes `value` as a prop and the app owns the state. Maximally flexible, trivially testable — and it pushes all the wiring, and every chance to get it wrong, onto every consuming team. **Uncontrolled with an escape hatch.** Owns state by default, but accepts an optional initial value, or a fully controlled `value` prop that takes over when supplied. This is the pattern most component libraries land on, and it is usually the right answer here too: easy adoption for the common case, a documented door for the hard case. Be explicit in the docs and the types about which mode is active, because a prop that is sometimes-controlled and sometimes-not is exactly the ambiguity that produces bug reports. Whichever you pick, expose behaviour rather than raw internals. Returning `{ isEnabled(name) }` lets you change the storage completely; returning the raw flags object means every consumer's code depends on that object's shape forever. ## Decision 3 — may two instances coexist? Because a consumer reads the *nearest* Provider, a second mount deeper in the tree creates an independent scope. You must decide what that means for your module and make the decision visible: - **Per-instance is the point.** Correct for anything inherently scoped — a form, a wizard, a widget instance. Document it and name the Provider so it reads as scoped. - **Exactly one is intended.** Then a nested second instance is a misconfiguration, and it will present as "my flag update didn't take effect over here" — an expensive bug to chase. Detect it: have the Provider read the context itself and warn during development when it finds an existing one. ```jsx function FeatureFlagProvider({ children }) { const existing = useContext(FlagContext); // private context if (process.env.NODE_ENV !== 'production' && existing) { console.warn('Nested <FeatureFlagProvider> detected; the inner one shadows the outer.'); } // … } ``` A related failure is worth pre-empting in the docs: if two copies of your package end up in the consumer's dependency tree, the Provider and the hook resolve *different* context objects and the hook reports a missing Provider even though one is mounted. Say so, and name the fix. ## The rest of the contract - **Failure mode.** Throw from the hook when no Provider is found, with a message naming the Provider. Silent nulls in a shared module become someone else's afternoon. - **Types.** Narrow inside the hook so consumers get a non-nullable type. A nullable type exported to twenty teams generates twenty redundant checks. - **Versioning.** Every exported name is a semver commitment. Fewer exports means more freedom to change internals — which is the whole point of keeping the context private. - **Environment constraints.** Be explicit about where the Provider may be mounted in the consumer's app, since a Provider carrying state and callbacks is inherently a client-side component. ## What interviewers listen for They want you to treat the module as an API with a blast radius: to say what you are deliberately *not* exporting and why, to pick a state-ownership model with its cost stated out loud, and to have an answer for two-instances and for duplicate copies of the package. "Export the context so people can do what they need" is the answer that fails this question.

  • A consuming team asks you to export the context object so they can stub it in tests. What do you offer instead?
    A purpose-built test Provider from the same module that takes the values they want to force. It gives them everything the raw context would, while keeping the value shape private and letting me evolve internals. If several teams want the same thing, that is a signal to make the seeding path part of the real Provider's API.
  • How would you migrate the value shape after teams already depend on it?
    If only the hooks are exported, I change the internals and adapt the hooks' return values, releasing under a minor version — most consumers notice nothing. If I add fields, that is additive. If I must remove one, the hook keeps returning it for a deprecation window with a development warning, then it goes. Having kept the context private is what makes any of that possible.
  • How do you detect a second instance mounted by mistake?
    Have the Provider read its own context during render. Finding an existing value means it is nested inside another instance, so warn in development naming both. If per-instance scoping is genuinely never valid for this module, that check is cheap insurance against a bug that otherwise presents as unexplained divergence between two parts of the app.

saying these in an interview costs you the question

  • Exports the context object so consumers can do anything
  • Assumes one Provider can ever be mounted
  • Returns the raw internal state object from the hook
  • Leaves the missing-Provider case returning null
  • Treats every export as free to change later

context