What you'll learn
Quick Answer
Context lets a value be read by any component beneath a provider without passing it through every level as props. Create it with createContext, wrap a subtree in the Provider, and read it with useContext. It solves prop drilling for genuinely global values such as theme, language and the current user. It is not a performance optimisation — every consumer re-renders when the value changes, so it is a poor fit for frequently changing state.
The Problem It Solves
Prop drilling is passing a value through components that do not use it, only to reach one deep in the tree.
<App user={user}>
<Layout user={user}>
<Sidebar user={user}>
<Profile user={user} /> ← only this one actually needs it
Layout and Sidebar gain a prop they do not care about, and any change to what Profile needs ripples through every intermediate component.
Context lets Profile read the value directly:
// 1. Create
const UserContext = createContext(null);
// 2. Provide, once, near the top
function App() {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={user}>
<Layout /> {/* no user prop anywhere below */}
</UserContext.Provider>
);
}
// 3. Consume, anywhere beneath
function Profile() {
const user = useContext(UserContext);
return <h1>{user?.name}</h1>;
}The value passed to createContext is the default, used only when a component reads the context with no provider above it — which is almost always a bug rather than an intended path.
The Pattern Worth Copying
Raw context usage spreads imports and null checks around the codebase. Wrapping it in a custom hook is the standard improvement.
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggle = useCallback(() => {
setTheme(t => t === 'light' ? 'dark' : 'light');
}, []);
// Memoised so the object identity is stable between renders
const value = useMemo(() => ({ theme, toggle }), [theme, toggle]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (ctx === null) {
throw new Error('useTheme must be used inside a ThemeProvider');
}
return ctx;
}Three things this buys you. Components import useTheme rather than both the hook and the context object. The error message names the actual mistake instead of producing a confusing null. And the provider owns its own state, so the file is self-contained.
The useMemo matters. Without it, { theme, toggle } is a new object on every render of the provider, so every consumer re-renders even when nothing changed. This is the most common Context performance mistake.
Every Consumer Re-renders
This is the limitation that decides whether Context suits your case.
When a context value changes, every component reading that context re-renders — even if it only uses one field of the object and that field did not change.
const value = { user, theme, notifications, cart }; // one big contextChange the cart and every component reading anything from this context re-renders, including ones that only display the theme. React.memo does not help, because context updates bypass it.
Two mitigations:
Split contexts by update frequency. A theme that changes twice a session and a cart that changes constantly should not share a provider.
<ThemeProvider> {/* rarely changes */}
<AuthProvider> {/* rarely changes */}
<CartProvider> {/* changes often — isolated */}
<App />
Separate the value from the setter. Components that only dispatch actions do not need to re-render when the data changes:
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}> {/* dispatch is stable */}
Because dispatch from useReducer never changes identity, consumers of the dispatch context never re-render from state updates.
When Context Is the Wrong Tool
Do not use it for values that change many times a second. Mouse position, scroll offset, form input on every keystroke — these will re-render every consumer on every change.
Do not use it to avoid passing two levels of props. Prop drilling through two or three components is fine and often clearer than the indirection. Context is worth it when the depth is real or the value is genuinely global.
Do not treat it as a state manager. Context is a transport mechanism — it moves a value down the tree. It has no reducers, no middleware, no devtools, no selective subscription. For complex application state, a dedicated library gives you those.
Consider composition instead. Often the real fix for prop drilling is passing children rather than data:
// Instead of Layout forwarding user to Sidebar to Profile
<Layout>
<Sidebar>
<Profile user={user} /> {/* composed at the top, no drilling */}
</Sidebar>
</Layout>This removes the intermediate props entirely without introducing context at all, and it is underused.
Good candidates for Context: current user and authentication, theme, language, feature flags, and a router. All are read widely and change rarely — exactly the profile Context is built for.
Common Mistakes
Creating a new value object every render. Covered above, and worth repeating because it is the most frequent one. Memoise the object, or pass a primitive.
Putting the provider too low. Only components inside the provider can read the context. A component rendering the provider cannot itself consume that context — it has to be a child.
Using the default value as real data. createContext({}) means a component outside the provider silently gets an empty object rather than an error, so the bug appears as "undefined property" somewhere far away. Default to null and throw in your custom hook.
One giant context for the whole app. It works, and it re-renders everything. Split by concern and by update frequency.
Reaching for Context before trying composition or lifting state. Most "I need global state" situations are two components that need a shared parent.
Forgetting it does not persist. Context is in-memory React state. A page refresh clears it, so anything that must survive — auth tokens, preferences — needs localStorage or a cookie behind it, with the context reading from that on mount.
