Context API Basics
React context for dependency injection — createContext, Provider, useContext, default values, and when context is the wrong tool.
- react
- context
- useContext
- state
Context passes data through the tree without threading props at every level. It is dependency injection for React: a Provider publishes a value; any descendant calls useContext to read the nearest Provider above it.
Context is not a global state management library. It solves prop drilling for shared ambient data (theme, locale, auth session, dial tone). For high-frequency updates across large trees, you still need structure — split contexts, memoize values, or use a real store.
Performance follow-up: Context Performance Pitfalls.
The problem: prop drilling
function App({ user }) {
return <Page user={user} />;
}
function Page({ user }) {
return <Dashboard user={user} />;
}
function Dashboard({ user }) {
return <Avatar user={user} />;
}
Page and Dashboard don’t care about user — they only forward it. Context lets Avatar subscribe without the middle props.
Model
const UserContext = createContext(null); // default if no Provider
function App({ user }) {
return (
<UserContext.Provider value={user}>
<Page />
</UserContext.Provider>
);
}
function Avatar() {
const user = useContext(UserContext);
if (!user) return null;
return <img src={user.avatarUrl} alt={user.name} />;
}
Rules of the model:
- Nearest Provider wins — nested providers override outer ones for their subtree.
- Default value applies only when no Provider exists above.
- Any change to the Provider’s
valuere-renders all consumers that read that context (in the classic API). - Context is read during render — it participates in the rules of hooks when using
useContext.
Full pattern: create, provide, consume
import { createContext, useContext, useMemo, useState } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(
() => ({
theme,
toggle: () => setTheme((t) => (t === 'light' ? 'dark' : 'light')),
}),
[theme]
);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) {
throw new Error('useTheme must be used within ThemeProvider');
}
return ctx;
}
Custom hooks around context:
- Encode the “must be under Provider” check once
- Hide the context object as a module private
- Give a clear API (
useTheme()vs rawuseContext)
This is the usual custom hooks design for app infrastructure.
Default values: useful vs footgun
const LocaleContext = createContext('en'); // soft default
function Label() {
const locale = useContext(LocaleContext);
return <span>{locale}</span>;
}
Defaults help tests and Storybook without a Provider. For required services (auth, data clients), prefer createContext(null) + throw in the hook so missing providers fail loud.
What belongs in context
| Good fit | Poor fit |
|---|---|
| Theme / density | Per-keystroke input text for a large tree |
| Current user / auth flag | Ephemeral hover state |
| i18n dictionary / locale | High-frequency mouse coordinates |
| Feature flags (read-mostly) | Unrelated mixed bag of all app state |
| DI for services (logger, tracker) | Avoiding learning props entirely |
If only two components deep need a value, props are clearer. Context earns its place when many distant consumers share the same ambient dependency.
Composition often beats context
// Instead of drilling or context for layout chrome:
function Shell({ sidebar, main }) {
return (
<div className="shell">
<aside>{sidebar}</aside>
<main>{main}</main>
</div>
);
}
<Shell sidebar={<Nav />} main={<Page />} />
Pass elements as props (children, sidebar) so parents don’t re-render middle layers just to forward data. See Avoid Prop Drilling with Composition.
Value identity matters
// New object every render → all consumers re-render every time
<UserContext.Provider value={{ user, logout }}>
Stabilize with useMemo / useCallback or split state so the object only changes when data changes. A Provider that re-renders because its parent re-rendered will still re-render consumers if value is a new reference — even if the fields are equal.
const value = useMemo(() => ({ user, logout }), [user, logout]);
Multiple contexts vs one mega-value
One context with { user, theme, cart, flags } forces every consumer of any field to re-render when any field changes. Split by update rate and concern:
<AuthProvider>
<ThemeProvider>
<CartProvider>{children}</CartProvider>
</ThemeProvider>
</AuthProvider>
Read only what you need in each component.
Context + state updates
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const value = useMemo(() => ({ user, setUser }), [user]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
Consumers that only need setUser still re-render when user changes if both live in one value. Advanced pattern: split UserContext and UserDispatchContext (state / dispatch) so dispatch consumers stay quiet. Same idea as React’s own docs for reducers + context.
Footguns
- Using context as a default app store without considering re-renders.
- Inline object
value={{…}}every render. - Missing Provider + silent default that masks wiring bugs.
- Deeply nesting 15 providers — consider composition or a store for truly global high-churn state.
- Reading context in a memoized child — context change still re-renders that child;
memodoes not block context. - Putting unstable functions in value without memo — breaks child
memoeven when data is stable.
Interview angle
Prompt: “What is React context and when would you use it?”
Strong answer: “Context lets me publish a value to a subtree so consumers can read it without prop drilling. I use it for ambient dependencies like theme, locale, and auth. Provider value identity controls consumer re-renders, so I memoize values and split contexts by concern. It’s not a replacement for local state or for specialized stores when updates are frequent and trees are large.”
Follow-ups: nearest provider wins; memo vs context; composition as alternative.
Related on this site
- Context Performance Pitfalls — re-render control
- useMemo and useCallback — stable values
- Custom Hooks Design —
useThemewrappers - Props vs State — data ownership
- useReducer for Complex State — often paired with context
- Avoid Prop Drilling with Composition
Further reading
- Passing Data Deeply with Context — react.dev
- useContext — react.dev
- createContext — react.dev
- Scaling Up with Reducer and Context
Related guides
- Props vs StateProps are inputs from the parent; state is data the component owns over time. How to choose, lift, and avoid the usual traps.
- useReducer for Complex StateWhen useReducer beats useState — action tables, pure reducers, dispatch stability, context pairing, and migration patterns.
- Accessibility Patterns in ReactPractical React a11y: labels, focus management, keyboard, live regions, and composition patterns that stay accessible.
- Avoid Prop Drilling with CompositionStop threading props through intermediates: children slots, inversion of control, and when context is the right escape hatch.
- Batching State UpdatesHow React 18+ batches setState in events, timeouts, and promises: when updates flush and why double setState still works.