Context API Guide: When and How to Use It
The Context API is a powerful tool for managing global state without prop drilling, but it is not always the right choice. Understanding when to use Context—and equally important, when not to use it—is essential for writing maintainable React applications. This guide covers ideal use cases, performance considerations, and alternatives.
Key Takeaways
- Context API is ideal for infrequently-changing global state — theming, authentication, language preferences, and application-wide settings
- Avoid Context for high-frequency updates — every context change causes all consumers to re-render; use Redux or Zustand for frequent updates
- Context is not a Redux replacement — it lacks middleware, time-travel debugging, and centralized state management features
- Composition can replace Context — for simple cases, component composition may be simpler and more performant than Context
- Use Context for dependencies, not data — it shines for configuration and cross-cutting concerns, not for frequently-changing app data
The Problem Context Solves: Prop Drilling
Prop drilling occurs when you pass props through many intermediate components that do not use those props themselves. For example:
// Prop drilling: passing theme through every component
function App() {
const [theme, setTheme] = useState('light');
return <Layout theme={theme} />;
}
function Layout({ theme }) {
return <Sidebar theme={theme} />;
}
function Sidebar({ theme }) {
return <Navigation theme={theme} />;
}
function Navigation({ theme }) {
return <ThemeButton theme={theme} />;
}
function ThemeButton({ theme }) {
return <button className={theme}>Toggle</button>;
}
The theme prop passes through Layout, Sidebar, and Navigation even though these components do not use it. The Context API eliminates this boilerplate:
// With Context, intermediate components are unaware of theme
const ThemeContext = React.createContext('light');
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Layout />
</ThemeContext.Provider>
);
}
function ThemeButton() {
const { theme } = useContext(ThemeContext);
return <button className={theme}>Toggle</button>;
}
Context solves this by allowing components to subscribe directly to global state without passing props through intermediaries.
Ideal Use Cases for Context API
1. Application Theming (Light/Dark Mode)
Theming is a perfect Context use case: the theme changes infrequently (maybe once per session), and many components at different nesting levels need it.
const ThemeContext = React.createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(t => t === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
// Usage: Any component can access theme
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={`header-${theme}`}>
<button onClick={toggleTheme}>Toggle Theme</button>
</header>
);
}
Theming changes rarely, benefits many components, and does not require Redux-level complexity.
2. User Authentication Status
Authentication state is another ideal case: it changes infrequently (login/logout), is needed throughout the app, and does not require high-frequency updates.
const AuthContext = React.createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Check if user is logged in on mount
checkAuthStatus().then(user => {
setUser(user);
setIsLoading(false);
});
}, []);
const login = (email, password) => {
// API call to authenticate
};
const logout = () => {
setUser(null);
};
return (
<AuthContext.Provider value={{ user, isLoading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
return useContext(AuthContext);
}
// Usage: Any component can check authentication
function Dashboard() {
const { user, isLoading } = useAuth();
if (isLoading) return <p>Loading...</p>;
if (!user) return <p>Please log in</p>;
return <h1>Welcome, {user.name}</h1>;
}
Authentication is perfect for Context: it rarely changes and is required across the entire app.
3. Language Preferences (Internationalization)
Language selection is another ideal case. It changes infrequently and is needed by many components.
const I18nContext = React.createContext();
export function I18nProvider({ children }) {
const [language, setLanguage] = useState('en');
const t = (key) => {
const translations = {
en: { greeting: 'Hello', goodbye: 'Goodbye' },
es: { greeting: 'Hola', goodbye: 'Adiós' }
};
return translations[language][key] || key;
};
return (
<I18nContext.Provider value={{ language, setLanguage, t }}>
{children}
</I18nContext.Provider>
);
}
Language settings are static per session, making them perfect for Context.
4. Application-Wide Settings
Any configuration or settings that affect the entire app and change infrequently are ideal for Context.
When NOT to Use Context
1. High-Frequency Updates
Problem: Every time a Context value changes, all components consuming that context re-render, even if their UI does not depend on the changed value.
// BAD: Mouse position updates hundreds of times per second
const MouseContext = React.createContext();
function App() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY }); // Updates very frequently
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return (
<MouseContext.Provider value={position}>
<ExpensiveComponent /> {/* Re-renders on every mouse move */}
</MouseContext.Provider>
);
}
Every mouse movement causes all consumers to re-render. For high-frequency updates, use a state management library like Redux, Zustand, or Jotai that has better performance optimizations.
2. Replacing Redux or a Full State Management Library
Problem: Context lacks features that Redux provides:
- Middleware — Context has no built-in way to handle async logic (Redux has thunks, sagas)
- Centralized Store — Redux provides a single, inspectable source of truth
- Time-Travel Debugging — Redux DevTools allow you to step through state changes
- Selectors — Redux allows memoized selectors to prevent unnecessary re-renders
- Normalization — Redux patterns support normalized data structures for complex apps
For apps with complex state, async operations, or large teams, use Redux or Zustand instead.
3. When Component Composition is Simpler
Problem: Context adds indirection. Sometimes, explicitly passing props or using component composition is clearer.
Example: Composition is simpler
// With Context (indirection)
const ModalContext = React.createContext();
export function ModalProvider({ children }) {
const [isOpen, setIsOpen] = useState(false);
return (
<ModalContext.Provider value={{ isOpen, setIsOpen }}>
{children}
</ModalContext.Provider>
);
}
// Better: Composition (explicit and clear)
function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Header onOpenModal={() => setIsOpen(true)} />
{isOpen && <Modal onClose={() => setIsOpen(false)} />}
</>
);
}
For simple cases, explicit prop passing is often clearer and requires no Context setup.
Comparison: Context vs. Prop Drilling vs. Composition
| Approach | Use Case | Trade-offs |
|---|---|---|
| Prop drilling | Simple, few components | Verbose; hard to maintain as app grows |
| Composition | Simple data/callbacks | Explicit and clear; requires component restructuring |
| Context API | Global, infrequent changes | Simple API; performance concerns with frequent updates |
| Redux/Zustand | Complex, frequent updates | More setup; powerful debugging and middleware |
Decision tree:
- Is the state needed by only a few nearby components? → Use
useStateand prop drilling. - Is the state global and changes infrequently? → Use Context.
- Is the state complex with frequent updates or async logic? → Use Redux or Zustand.
- Can you restructure components to avoid passing data through many levels? → Use composition.
Best Practices for Context
Create custom hooks — wrap useContext in a custom hook to simplify usage:
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used inside ThemeProvider');
}
return context;
}
Split contexts by concern — create separate contexts for different concerns (theme, auth, i18n) rather than one monolithic context. This allows components to subscribe only to what they need.
Memoize context values — prevent unnecessary re-renders by memoizing the context value:
const value = useMemo(() => ({ theme, toggleTheme }), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
Provider hierarchy — nest providers in a logical order, typically at the app root:
function App() {
return (
<ThemeProvider>
<AuthProvider>
<I18nProvider>
<MainApp />
</I18nProvider>
</AuthProvider>
</ThemeProvider>
);
}
Frequently Asked Questions
Can I use Context for form state?
Context can work for form state if the form is large and deeply nested. However, for most forms, local useState is simpler. If you have many forms, consider a form library like React Hook Form instead.
How do I avoid all consumers re-rendering when context changes?
Split your context into separate values for different concerns. Alternatively, use a memoized selector or library like use-context-selector to subscribe to specific parts of context.
Is Context faster than Redux?
No. Redux with memoized selectors is generally faster for large apps because it prevents unnecessary re-renders. Context is simpler for small use cases but has performance limitations at scale.
Can I use multiple contexts?
Yes. Use multiple contexts for different concerns:
const AuthContext = createContext();
const ThemeContext = createContext();
function App() {
return (
<AuthProvider>
<ThemeProvider>
<MainApp />
</ThemeProvider>
</AuthProvider>
);
}
Components can consume multiple contexts as needed.
What is the performance cost of Context?
Every time the Context value changes, all components consuming that context re-render, even if the specific value they use did not change. For infrequent changes (theming, auth), this is negligible. For frequent changes, performance degrades.