Back to React
React
medium
mid

How would you implement the React Context API for global state management?

createContext → a Provider component holding state (useState/useReducer) → consumers read via useContext. Key pitfalls: every consumer re-renders when the value changes, the value object must be stable, and unrelated state should be split into separate contexts.

5 min read·~8 min to think through

Context shares state across the tree without prop-drilling. The implementation is short; the senior content is the pitfalls.

The implementation

jsx
// 1. Create the context
const ThemeContext = createContext(null);

// 2. A Provider component that owns the state
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");

  // memoize the value so consumers don't re-render on unrelated parent renders
  const value = useMemo(() => ({ theme, setTheme }), [theme]);

  return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}

// 3. A custom hook for ergonomics + safety
function useTheme() {
  const ctx = useContext(ThemeContext);
  if (ctx === null) throw new Error("useTheme must be used within ThemeProvider");
  return ctx;
}

// 4. Consume
function Toggle() {
  const { theme, setTheme } = useTheme();
  return <button onClick={() => setTheme(t => t === "light" ? "dark" : "light")}>{theme}</button>;
}

For complex state, swap useState for useReducer inside the provider.

The pitfalls that matter

1. Every consumer re-renders when the value changes. Context has no selector — when the provider value changes, all useContext consumers re-render, even ones that only use an unchanged part. This is the #1 Context gotcha.

2. Stabilize the value. value={{ theme, setTheme }} creates a new object every render → every consumer re-renders every time the provider re-renders. Wrap it in useMemo.

3. Split contexts by concern and by change frequency. Don't put theme, auth, cart, and toasts in one context. Separate them — and separate rarely-changing data from frequently-changing data — so a fast-changing slice doesn't re-render consumers of a slow one. A common pattern: split state and dispatch into two contexts so dispatch-only consumers never re-render.

4. Context is not a Redux replacement for high-frequency state. For complex, frequently-updated global state, a real store (Zustand/Redux) with selectors avoids the re-render-everything problem.

5. Provide a guard hook that throws if used outside the provider — catches a whole class of bugs.

The framing

"createContext, a Provider component that owns state via useState/useReducer, consumers via a custom useContext hook that guards against missing provider. The pitfalls are what matter: Context has no selectors, so every consumer re-renders on any value change — which means you must useMemo the value object and split contexts by concern and change frequency. For high-frequency global state, Context isn't the right tool; a store with selectors is."

Follow-up questions

  • Why does every consumer re-render when the context value changes?
  • Why split state and dispatch into separate contexts?
  • When is Context not enough and you need Redux/Zustand?
  • How would you add selector-like behavior to Context?

Common mistakes

  • Passing an inline object as value — re-renders all consumers every render.
  • Putting all global state in one giant context.
  • Using Context for high-frequency state and getting widespread re-renders.
  • No guard hook, so using a consumer outside the provider fails silently or oddly.

Performance considerations

  • Context's lack of selectors means any value change re-renders every consumer. Memoize the value, split contexts by change frequency, and split state vs dispatch. For genuinely hot global state, use an external store with selector subscriptions.

Edge cases

  • Nested providers of the same context — inner one wins for its subtree.
  • A consumer outside any provider — gets the default value.
  • Frequently-changing slice forcing re-renders of slow-slice consumers.

Real-world examples

  • Theme, locale, and current-user contexts — low-frequency, perfect for Context.
  • Splitting a reducer's state and dispatch into two contexts so action-dispatching components don't re-render.

Senior engineer discussion

Seniors implement it cleanly with a guard hook and memoized value, but spend most of the answer on the re-render model: no selectors, split by concern and frequency, state/dispatch separation, and knowing the boundary where an external store becomes the right choice.

Related questions