Back to React
React
easy
mid

How would you implement a polyfill for the useContext hook?

`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.

4 min read·~20 min to think through

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

jsx
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:

  1. createContext returns an object { Provider, _currentValue, ... }.
  2. Provider pushes its value onto a per-context stack as React walks the fiber tree during render; pops on exit.
  3. 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:

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 value changes. 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:

js
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 useSyncExternalStore for 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.

Senior engineer discussion

Seniors articulate the stack model, note that real React weaves it into fiber, and know the practical traps: memoize value, split contexts, reach for external stores when fine-grained subscription matters.

Related questions