Back to React
React
medium
mid

How would you implement the React Context API for a real feature?

createContext returns a { Provider, Consumer }. The Provider wraps a subtree with a value prop; descendants read via useContext(MyContext). Internally React walks up the fiber tree on read to find the nearest Provider; subscribers are tracked so they re-render when value changes. Pitfalls: every value change re-renders every consumer (split contexts), inline object values cause unnecessary updates (memoize).

7 min read·~10 min to think through

Context lets descendants read shared values without prop drilling.

API

tsx
import { createContext, useContext } from 'react';

const ThemeContext = createContext<'light' | 'dark'>('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  const theme = useContext(ThemeContext);
  return <div className={theme}>...</div>;
}

How it works under the hood

  1. createContext returns a context object with internal slots (a _currentValue and a list of subscribed fibers).
  2. When a Provider renders, it updates the context's current value.
  3. When a component calls useContext, React walks up the fiber tree to find the nearest matching Provider and subscribes the calling fiber.
  4. When the Provider's value changes, React schedules a re-render for every subscribed consumer — bypassing React.memo and shouldComponentUpdate.

Patterns

Auth context with a hook wrapper:

tsx
const AuthContext = createContext<{ user: User | null; login: () => void } | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const login = useCallback(async () => { setUser(await fetchMe()); }, []);
  const value = useMemo(() => ({ user, login }), [user, login]);
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth outside AuthProvider');
  return ctx;
}

The useMemo is the critical bit — without it, a new object is created every render, so every consumer re-renders.

Split contexts to limit re-renders

If you have read-rarely and read-often state in one provider, every change re-renders everyone. Split:

tsx
<UserContext.Provider value={user}>
  <DispatchContext.Provider value={dispatch}>
    {children}
  </DispatchContext.Provider>
</UserContext.Provider>

Components that only need dispatch don't re-render when user changes.

Context selector pattern

React doesn't have built-in selectors. Workarounds:

  1. Split contexts (above).
  2. use-context-selector library — re-renders only when the selected slice changes.
  3. External stores (Zustand, Jotai, Redux) — designed for selective subscription.

When to use context

  • Theme, locale, current user — read by many, rarely change.
  • Dispatch / actions — stable identities; cheap to provide.
  • Compound component config — Tabs sharing state with its TabList children.

When NOT to use context

  • Server state — use React Query.
  • State that updates often + many consumers — performance death by re-render. Reach for Zustand/Jotai/Redux.
  • Just to avoid prop drilling 2 levels — drilling is fine for short distances.

Default value

createContext(defaultValue) — used when no Provider is found above. Useful for testing components in isolation.

Performance limits

  • Context updates re-render every consumer; no selector built in.
  • Memoize the Provider value with useMemo or you create a new object every render.
  • For hot updates (mouse position, scroll), context is the wrong tool.

React 19 'use' API

React 19 adds use(MyContext) — can be called conditionally and from server components. Same mechanism, broader API.

Follow-up questions

  • Why does context not have built-in selectors?
  • How do you avoid re-rendering all consumers when one slice changes?
  • When would you reach for Zustand instead of context?

Common mistakes

  • Fresh object/function as Provider value each render — every consumer re-renders.
  • Using context for state that updates many times per second.
  • Forgetting to throw when consumed outside the Provider — silent null reads.

Performance considerations

  • Every consumer re-renders when the context value changes. Memoize the value. Split contexts by update frequency. For very hot state, use a state library that supports selectors.

Edge cases

  • useContext returns the default value when no Provider exists — easy to miss.
  • Context bypasses React.memo — you can't optimize a consumer with memo if the context changes.
  • Multiple Providers of the same context: the nearest wins.

Real-world examples

  • Almost every React app uses context for auth, theme, i18n, router. State libraries (Redux, Zustand) use a tiny context at the root to expose the store, but the store handles subscriptions itself.

Senior engineer discussion

Senior framing: context is for dependency injection of values that change rarely. The moment you find yourself dispatching frequent updates through it and worrying about re-renders, you've outgrown context and need a real store.

Related questions