ReactJs Context API
The Context API provides a way to share data across the component tree without manually passing props through every level. It solves the “prop drilling” problem, where props must be passed through multiple intermediate components that don’t use them, just to reach deeply nested components that need the data.
The Prop Drilling Problem
In a typical React application without Context, passing data from a top-level component to a deeply nested component requires threading props through all intermediate components:
function App() {
const [user, setUser] = useState({ name: 'John', role: 'admin' });
return <Dashboard user={user} />;
}
function Dashboard({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <UserMenu user={user} />;
}
function UserMenu({ user }) {
return <div>{user.name} ({user.role})</div>;
}
In this example, Dashboard and Sidebar don’t use the user prop; they only pass it down. This becomes cumbersome as the application grows and the component tree deepens.
Creating and Using Context
The Context API consists of three main parts: creating a context, providing a value, and consuming that value.
Creating a Context
Use React.createContext() to create a new context:
import { createContext } from 'react';
const UserContext = createContext(null);
The argument to createContext is the default value, used only when a component doesn’t have a matching Provider above it in the tree.
Providing Context
Wrap your component tree with a Context Provider to make the value available to all descendant components:
function App() {
const [user, setUser] = useState({ name: 'John', role: 'admin' });
return (
<UserContext.Provider value={user}>
<Dashboard />
</UserContext.Provider>
);
}
Consuming Context
Components can access the context value using the useContext Hook:
import { useContext } from 'react';
function UserMenu() {
const user = useContext(UserContext);
return <div>{user.name} ({user.role})</div>;
}
Now UserMenu can access the user data directly without it being passed through intermediate components.
Complete Example
Here’s a full example demonstrating Context for theme management:
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext(null);
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value=>
<div className={`app ${theme}`}>
<Toolbar />
<MainContent />
</div>
</ThemeContext.Provider>
);
}
function Toolbar() {
return (
<div className="toolbar">
<ThemeToggle />
</div>
);
}
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
);
}
function MainContent() {
const { theme } = useContext(ThemeContext);
return (
<div className="content">
<p>Current theme: {theme}</p>
</div>
);
}
Multiple Contexts
Applications can use multiple contexts for different types of data. Each context operates independently:
const UserContext = createContext(null);
const ThemeContext = createContext('light');
const LanguageContext = createContext('en');
function App() {
return (
<UserContext.Provider value={currentUser}>
<ThemeContext.Provider value={theme}>
<LanguageContext.Provider value={language}>
<MainApp />
</LanguageContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}
Components can consume multiple contexts:
function Header() {
const user = useContext(UserContext);
const theme = useContext(ThemeContext);
const language = useContext(LanguageContext);
return <header className={theme}>...</header>;
}
Context Best Practices
When to Use Context
Context is ideal for data that is truly global or shared across many components:
- Current authenticated user
- Theme preferences (light/dark mode)
- Locale/language settings
- Application configuration
- UI state (modals, notifications)
When Not to Use Context
Avoid Context for:
- Passing props down only 1-2 levels (use props instead)
- Frequently changing data that affects only a few components
- Data that doesn’t need to be global
Optimizing Context Updates
Context triggers re-renders in all consumers when the value changes. To optimize performance:
Split contexts by update frequency:
// Frequently updated
const StateContext = createContext();
// Rarely updated
const DispatchContext = createContext();
function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
Memoize context values:
function App() {
const [user, setUser] = useState(null);
const contextValue = useMemo(() => ({
user,
setUser
}), [user]);
return (
<UserContext.Provider value={contextValue}>
{children}
</UserContext.Provider>
);
}
Custom Context Hooks
Encapsulate context logic in custom hooks for better developer experience:
const UserContext = createContext(null);
export function UserProvider({ children }) {
const [user, setUser] = useState(null);
const login = (userData) => setUser(userData);
const logout = () => setUser(null);
const value = useMemo(() => ({
user,
login,
logout,
isAuthenticated: !!user
}), [user]);
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}
export function useUser() {
const context = useContext(UserContext);
if (context === null) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
// Usage
function UserProfile() {
const { user, logout } = useUser();
return (
<div>
<p>{user.name}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
This pattern provides type safety, better error messages, and a cleaner API for consuming context.
Context vs Other State Management
Context is built into React and sufficient for many applications. However, for complex state management needs, consider dedicated libraries:
- Redux - Predictable state container with time-travel debugging
- MobX - Observable state management
- Zustand - Lightweight state management
- Jotai - Atomic state management
- Recoil - Experimental state management from Facebook
Use Context when:
- State is relatively simple
- Updates are infrequent
- You want to minimize dependencies
- The React ecosystem is sufficient
Use dedicated libraries when:
- State logic is complex
- You need advanced features (middleware, dev tools, persistence)
- Performance optimization is critical
- Your team is already familiar with a specific solution
Common Pitfalls
Avoid Context for All State
Not all state needs to be in Context. Keep state as local as possible, and only lift it to Context when multiple unrelated components need access.
Don’t Overuse Context
Creating too many contexts can make the application harder to understand. Group related data together, but don’t create a single massive context for everything.
Remember Re-render Behavior
Every consumer of a Context re-renders when the context value changes, even if they only use a small part of the value. Split contexts or use more targeted state management for frequently changing data.