In a React 19 app, code outside the component tree — an HTTP client interceptor, a WebSocket message handler, an analytics module — needs to read and update the current auth token. Why can React Context not serve that, and how would you structure it instead?
answer
- only readable during render, inside the tree
- no getContextValue from a module
- invert who owns the truth
- subscribe/getSnapshot module, React binds to it
- publish the instance, not the value
basics
~20 sContext values are only readable from a component rendering under the provider, during render, so plain modules cannot reach them. Keep the token in a module-level store that any code can read and write, and expose it to components with useSyncExternalStore.
solid answer
~40 sContext is reachable only from inside the tree: `useContext` and `use(Context)` run during a component's render, and there is no function that hands a plain module the current value. The only way to change a context value is to re-render its provider, which non-React code cannot do directly either. So I invert the ownership — the token's source of truth becomes a small module with `getSnapshot`, `setToken` and `subscribe`. The interceptor and the socket handler import that module and read or write it synchronously, with no React involved. Components bind to it through `useSyncExternalStore(subscribe, getSnapshot)`, which is React's sanctioned way to read a mutable external source and stay consistent with concurrent rendering. Context can still play its own role: publish the *store instance* so tests and per-request instances can swap it.
code
javascript · 14 lineslet token = null;
const listeners = new Set();
export const authStore = {
getSnapshot: () => token,
setToken(next) {
token = next;
listeners.forEach((listener) => listener());
},
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
};go deeper
Know that hooks including useContext only run inside a component while React renders it, so a plain module or a callback cannot call one. Say that out loud before proposing a design.
Explain why mirroring the value into a global with an effect is fragile — late updates, unmount gaps, duplicated truth — and describe the module store with subscribe and snapshot that React binds to instead.
Show the judgment of inverting ownership: the state belongs where its consumers are, and React becomes one subscriber among several. Cover snapshot stability and per-request isolation on the server.
Own the boundary as a contract: which values are injected through the tree and which live in modules other code may import. Set the convention early, since a module singleton that leaks across server requests is a cross-team incident, not a component bug.
## The constraint that decides the design A context value lives in the render tree. `useContext(AuthContext)` and `use(AuthContext)` are hook calls: they execute while React renders a component that sits under the provider, and they resolve to whatever that provider published on its most recent render. React exposes no API that hands the current value of a context to a plain module, and nothing outside the tree can push a new value in — a context value changes only when its provider re-renders. That is a deliberate design, not an oversight. Context is a dependency-injection channel scoped to a tree, and "scoped to a tree" is exactly what an axios interceptor or a socket callback is not. ## Why the obvious workarounds fail **Exporting the context object and reading from it.** The context object is a marker React uses during rendering, not a box holding the value. Reaching for undocumented internals to get at it breaks across versions and gives you the wrong value under concurrent rendering, where different parts of a render pass can be at different points. **Copying the value into a module variable from inside a component.** A `useEffect` that writes `currentToken = token` on every change looks like it works and mostly does — until the non-React code runs before the first commit, or after the component unmounts, or in a second React root. You have created a second copy of the truth that is updated late and never cleaned up. **Passing a setter down through Context to the module.** Now the module holds a callback that only works while a particular provider is mounted, and a stale one after remounts. All three fail for the same reason: React state is being asked to be the source of truth for something that has consumers outside React's lifetime. ## The shape that works: invert the ownership Make a plain module the source of truth. It needs three things — read, write, and subscribe: ```js let token = null; const listeners = new Set(); export const authStore = { getSnapshot: () => token, setToken(next) { token = next; listeners.forEach((l) => l()); }, subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); }, }; ``` The interceptor and the socket handler now `import { authStore }` and call `authStore.getSnapshot()` or `authStore.setToken(next)` directly. No React, no timing question, no mounted-component precondition. Components bind with `useSyncExternalStore`, which takes the subscribe function and the snapshot getter and returns the current value, re-rendering the component when the store notifies: ```jsx function useToken() { return useSyncExternalStore(authStore.subscribe, authStore.getSnapshot); } ``` Two details matter. `getSnapshot` must return a value that is stable between notifications — a primitive like a token string is safe; returning a freshly built object each call causes React to see a change every time. And on a server render there is no subscription, so a store meant to render on the server supplies a server snapshot as the third argument. ## Where Context still belongs None of this argues against Context — it argues about *what* you publish through it. Publishing the token means only the tree can see it. Publishing the store instance gives you the injection benefits without the constraint: ```jsx <AuthStoreContext value={authStore}>{children}</AuthStoreContext> ``` The value is a stable reference, so the context never causes an update; components pull the instance and subscribe. Tests render a different instance, and a server that must isolate requests creates one store per request instead of relying on a module-level singleton. ## The rule to take away Ask who the consumers are. If every consumer is a component inside one subtree, Context is enough and simplest. The moment a consumer is a module, a callback registered with a browser API, or an interceptor that runs with no component on the stack, the state has left React's jurisdiction — put it in a store and let React subscribe, rather than putting it in React and trying to smuggle it out.
- Why is a useEffect that mirrors the context value into a module variable not equivalent?Because the mirror is updated after commit and only while the component is mounted. Non-React code that runs before the first commit, after unmount, or in a different root reads a stale or empty value. It also duplicates the source of truth, so the two can disagree — and nothing tells you when they do.
- What goes wrong if getSnapshot builds a new object on every call?React compares the snapshot with Object.is on each check, so a freshly allocated object never matches the previous one. That reads as a perpetual change: the component re-renders in a loop, and in development React warns about it. Return a cached reference and replace it only when the underlying data actually changes.
- Does putting the store instance in Context cost anything at runtime?Practically nothing. The published value is one stable object reference created outside the render, so the provider hands down the same value on every render and no consumer is ever woken by the context itself. All update traffic goes through the store's own subscriptions, which are per-consumer.
saying these in an interview costs you the question
- Just export the context and read its value in the module
- Copy the token into a global inside useEffect
- Non-React code can call useContext outside a component
- A module singleton is safe on the server too
- Return a fresh object from getSnapshot every call