React Context API Guide
Learn how to use React's Context API to share state across your component tree without prop drilling — with patterns, performance tips, and common mistakes to avoid.
What you'll learn
- ✓When Context is the right tool
- ✓Creating providers and consumers
- ✓Splitting contexts for performance
- ✓Combining Context with useReducer
- ✓Pitfalls that cause unnecessary rerenders
Prerequisites
- •Familiarity with JavaScript basics
The Context API in React lets you share values across a component tree without manually threading props through every level. It is the built-in answer to prop drilling. This tutorial covers when to reach for Context, how to use it well, and what to watch out for.
Why Context Exists
Imagine a deeply nested button that needs the current theme. Without Context, you would pass theme as a prop through every component between the root and the button. With Context, the theme is defined once at the top and any descendant can read it.
Context is best for values that rarely change but many components need — themes, locale, current user, or feature flags. It is not a replacement for general state management.
Creating a Context
You create a context with createContext. The argument is the default value used when there is no matching provider above in the tree.
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext('light');
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
Wrapping useContext in a custom hook gives you a clean API and a single place to throw errors if the hook is used outside the provider.
Consuming the Context
Any descendant of the provider can read the value with useContext. There is no prop drilling and no intermediate component knows the context exists.
function ThemedButton() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Theme: {theme}
</button>
);
}
The button rerenders whenever the provider value changes. That last word — changes — has a sharp edge that we will look at next.
The Rerender Trap
Every consumer of a context rerenders whenever the provider’s value prop changes by reference. If you pass an inline object literal, you create a new reference on every render.
// Bad: new object every render
<ThemeContext.Provider value={{ theme, setTheme }}>
Wrap the value in useMemo to stabilize the reference and avoid waterfalls of pointless rerenders.
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
This single change can transform performance in apps with many context consumers.
Splitting Contexts
A common pattern is to split one context into two — one for the value, one for the setter. Components that only need the setter never rerender when the value changes.
const ThemeStateContext = createContext('light');
const ThemeSetterContext = createContext(() => {});
This adds boilerplate but pays off when consumers are expensive to rerender, like virtualized lists.
Combining with useReducer
For more structured state, pair Context with useReducer. The provider exposes state and dispatch, and components dispatch actions instead of calling setters.
function appReducer(state, action) {
switch (action.type) {
case 'login': return { ...state, user: action.user };
case 'logout': return { ...state, user: null };
default: return state;
}
}
export function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, { user: null });
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
Dispatch never changes reference, so consumers of DispatchContext rerender only when state really changes.
When Not to Use Context
Context is not always the right tool. If your value changes frequently — say, mouse position or scroll offset — every consumer will rerender constantly. Use a state library like Zustand or Jotai, or pass props directly.
Context also is not a global event bus. Avoid stuffing unrelated values into one big context just because it is convenient.
Wrapping Up
The Context API is a clean solution to prop drilling for values that change rarely. Memoize the provider value, split contexts when needed, and reach for a state library when frequency outgrows what Context can handle. Used wisely, Context keeps your component tree simple and shallow.
Related articles
- JavaScript React Custom Hooks Patterns
Learn powerful patterns for building React custom hooks — extracting logic, composing hooks, handling cleanup, and designing clean APIs your team will love.
- JavaScript React Hooks Deep Dive: useState to useRef and When to Use Each
A practical deep dive into useState, useEffect, useMemo, useCallback, and useRef. Learn when to reach for each hook and the traps that ruin React code.
- JavaScript React useEffect Common Mistakes
Avoid the most common useEffect mistakes — missing dependencies, infinite loops, stale closures, and effects that should not be effects in the first place.
- React React Context vs Redux: When to Use Which
A practical comparison of React Context and Redux: rendering model, performance, devtools, and concrete heuristics for picking the right tool.