Implement a polyfill for useContext
`useContext` reads the value from the nearest `<Context.Provider>` ancestor. Polyfill conceptually: each Context owns a stack of currently-rendering Provider values; useContext returns the top. In React's real implementation it's woven into fiber — but for an interview, a minimal store + Provider that pushes/pops via a render-tracking mechanism suffices.
useContext looks simple but its implementation is part of React's fiber architecture. The interview wants you to explain the model, then sketch a minimal version.
What useContext actually does
const ThemeCtx = createContext("light");
function Child() {
const theme = useContext(ThemeCtx); // reads from nearest Provider ancestor
return <div className={theme} />;
}
<ThemeCtx.Provider value="dark"><Child/></ThemeCtx.Provider>Reads the value of the nearest <ThemeCtx.Provider> ancestor in the React tree (or the default if none).
Real React: tied to fiber
React's actual implementation:
- createContext returns an object
{ Provider, _currentValue, ... }. - Provider pushes its value onto a per-context stack as React walks the fiber tree during render; pops on exit.
- useContext(ctx) reads
ctx._currentValue— which is whatever was last pushed during the current render path.
This works because React renders synchronously top-down through fibers; the stack mirrors the tree.
Minimal polyfill
You can't fully reproduce it without a renderer, but here's the conceptual sketch in plain JS:
function createContext(defaultValue) {
const stack = [defaultValue];
const ctx = {
_stack: stack,
Provider({ value, children }) {
stack.push(value);
try { return render(children); }
finally { stack.pop(); }
},
};
ctx.useContext = () => stack[stack.length - 1];
return ctx;
}
function useContext(ctx) { return ctx._stack[ctx._stack.length - 1]; }The Provider pushes on enter, pops on exit; the stack's top is always the current value during render.
Why this is a sketch, not real React
- Re-renders on value change — React notifies all consumers when
valuechanges. The minimal version doesn't subscribe. - Bailout on equality — React skips re-rendering consumers when value is referentially equal.
- Concurrent rendering — interruptible work; React uses a more sophisticated mechanism than a literal stack.
- Multiple providers of different contexts coexist — independent stacks per context.
Subscription-based polyfill (closer to behavior)
For a runtime closer to real React:
function createContext(defaultValue) {
const subscribers = new Set();
let currentValue = defaultValue;
return {
Provider({ value, children }) {
if (value !== currentValue) {
currentValue = value;
subscribers.forEach((fn) => fn(value));
}
return children;
},
useContext() {
const [v, setV] = useState(currentValue);
useEffect(() => {
subscribers.add(setV);
return () => subscribers.delete(setV);
}, []);
return v;
},
};
}This is closer to Zustand-style external store than React's real fiber implementation — but it captures the consumer-subscription idea.
Common interview points
- Default value only matters when there's no Provider above.
- Provider value is referential —
<Provider value={{x:1}}>re-renders every consumer every time. Memoize the value. - All consumers re-render on value change — there's no fine-grained subscription. Split contexts or use
useSyncExternalStorefor fine-grained reactivity.
Interview framing
"useContext reads the value from the nearest matching <Provider> ancestor — or the default if none. Conceptually, each context has a stack: the Provider pushes its value on enter, pops on exit; useContext returns the top. Real React implements this through the fiber tree during render, not a literal global stack, but the model is the same. A minimal polyfill can mimic this with a stack manipulated by Provider component rendering; a closer-to-real version uses a subscription set notifying consumers on value change — basically a tiny external store. The two interview gotchas: every consumer re-renders on value change (memoize the value object), and there's no fine-grained selection — split contexts when that hurts."
Follow-up questions
- •Why does `<Provider value={{x:1}}>` re-render every consumer every render?
- •How does React skip consumers that don't depend on the changed value? (It doesn't — useContext is all-or-nothing.)
- •Why won't a setState-based polyfill be sufficient for concurrent mode?
- •How does this relate to `useSyncExternalStore`?
Common mistakes
- •Treating Provider value as referentially stable when it's a fresh object each render.
- •Expecting fine-grained re-renders.
- •Forgetting that without a Provider, the default value is used.
Performance considerations
- •Every consumer re-renders on value change. Memoize the value object; split contexts to reduce blast radius; for hot state, use an external store (Zustand) or `useSyncExternalStore`.
Edge cases
- •Nested providers of the same context — closest wins.
- •No provider — default value.
- •Provider value changing every render — performance trap.
Real-world examples
- •Theme/locale/user-session providers.
- •React Router's Location/Match contexts.