How do you implement 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.
Context shares state across the tree without prop-drilling. The implementation is short; the senior content is the pitfalls.
The implementation
// 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.