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).
Context lets descendants read shared values without prop drilling.
API
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
- createContext returns a context object with internal slots (a _currentValue and a list of subscribed fibers).
- When a Provider renders, it updates the context's current value.
- When a component calls useContext, React walks up the fiber tree to find the nearest matching Provider and subscribes the calling fiber.
- 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:
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:
<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:
- Split contexts (above).
- use-context-selector library — re-renders only when the selected slice changes.
- 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.