Context API
Context provides a way to share data across the component tree without manually passing props at every level. It’s React’s built-in solution for “global” state.The Problem: Prop Drilling
Without Context, passing data to deeply nested components requires threading props through every intermediate component:Copy
┌─────────────────────────────────────────────────────────────┐
│ App │
│ theme="dark" │
│ │ │
│ ▼ │
│ Layout │
│ theme={theme} ← Doesn't use it │
│ │ │
│ ▼ │
│ Sidebar │
│ theme={theme} ← Doesn't use it │
│ │ │
│ ▼ │
│ Menu │
│ theme={theme} ← Doesn't use it │
│ │ │
│ ▼ │
│ MenuItem │
│ theme={theme} ← Finally uses it! │
└─────────────────────────────────────────────────────────────┘
Copy
┌─────────────────────────────────────────────────────────────┐
│ ThemeContext.Provider │
│ value="dark" │
│ │ │
│ ┌─────────┴─────────┐ │
│ ▼ ▼ │
│ Layout Sidebar │
│ │ │ │
│ ▼ ▼ │
│ Header MenuItem │
│ useContext() useContext() │
│ ↑ ↑ │
│ └────────────────────┘ │
│ Direct access to "dark" │
└─────────────────────────────────────────────────────────────┘
Creating and Using Context
Step 1: Create the Context
Copy
// ThemeContext.js
import { createContext } from 'react';
// Default value (used when no Provider is found)
export const ThemeContext = createContext('light');
Step 2: Provide the Context
Copy
// App.jsx
import { ThemeContext } from './ThemeContext';
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={theme}>
<Layout />
</ThemeContext.Provider>
);
}
Step 3: Consume the Context
Copy
// MenuItem.jsx
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
function MenuItem({ label }) {
const theme = useContext(ThemeContext);
return (
<li className={`menu-item ${theme}`}>
{label}
</li>
);
}
Complete Example: Theme Context
Here’s a full implementation with toggling:Copy
// contexts/ThemeContext.jsx
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
const value = {
theme,
toggleTheme,
isDark: theme === 'dark'
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
Copy
// App.jsx
import { ThemeProvider } from './contexts/ThemeContext';
function App() {
return (
<ThemeProvider>
<div className="app">
<Header />
<Main />
</div>
</ThemeProvider>
);
}
Copy
// components/ThemeToggle.jsx
import { useTheme } from '../contexts/ThemeContext';
function ThemeToggle() {
const { theme, toggleTheme, isDark } = useTheme();
return (
<button onClick={toggleTheme}>
{isDark ? '☀️ Light Mode' : '🌙 Dark Mode'}
</button>
);
}
Best Practice: Always create a custom hook (
useTheme) instead of exporting the context directly. This:- Provides better error messages
- Makes refactoring easier
- Hides implementation details
Real-World Pattern: Auth Context
Copy
// contexts/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check for existing session
const checkAuth = async () => {
try {
const token = localStorage.getItem('token');
if (token) {
const response = await fetch('/api/auth/me', {
headers: { Authorization: `Bearer ${token}` }
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
}
}
} catch (error) {
console.error('Auth check failed:', error);
} finally {
setLoading(false);
}
};
checkAuth();
}, []);
const login = async (email, password) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (!response.ok) {
throw new Error('Login failed');
}
const { user, token } = await response.json();
localStorage.setItem('token', token);
setUser(user);
return user;
};
const logout = () => {
localStorage.removeItem('token');
setUser(null);
};
const value = {
user,
loading,
isAuthenticated: !!user,
login,
logout
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
Using Auth Context
Copy
// components/Navbar.jsx
function Navbar() {
const { user, isAuthenticated, logout } = useAuth();
return (
<nav>
<Logo />
{isAuthenticated ? (
<>
<span>Welcome, {user.name}</span>
<button onClick={logout}>Logout</button>
</>
) : (
<Link to="/login">Login</Link>
)}
</nav>
);
}
Copy
// pages/LoginPage.jsx
function LoginPage() {
const { login } = useAuth();
const navigate = useNavigate();
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
await login(email, password);
navigate('/dashboard');
} catch (err) {
setError('Invalid credentials');
}
};
return (/* form */);
}
Multiple Contexts
You can compose multiple contexts:Copy
// App.jsx
function App() {
return (
<AuthProvider>
<ThemeProvider>
<NotificationProvider>
<Router>
<Layout />
</Router>
</NotificationProvider>
</ThemeProvider>
</AuthProvider>
);
}
Alternative: Compose Providers
Copy
// contexts/AppProviders.jsx
function AppProviders({ children }) {
return (
<AuthProvider>
<ThemeProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</ThemeProvider>
</AuthProvider>
);
}
// App.jsx
function App() {
return (
<AppProviders>
<Router>
<Layout />
</Router>
</AppProviders>
);
}
Performance Optimization
The Re-render Problem
When context value changes, ALL consuming components re-render, even if they only use part of the context:Copy
// ❌ Problem: Updating `cart` re-renders ThemeToggle
const AppContext = createContext();
function AppProvider({ children }) {
const [theme, setTheme] = useState('light');
const [cart, setCart] = useState([]);
return (
<AppContext.Provider value={{ theme, setTheme, cart, setCart }}>
{children}
</AppContext.Provider>
);
}
function ThemeToggle() {
const { theme, setTheme } = useContext(AppContext);
// Re-renders when cart changes! 😱
}
Solution 1: Split Contexts
Copy
// ✅ Separate concerns into different contexts
const ThemeContext = createContext();
const CartContext = createContext();
function App() {
return (
<ThemeProvider>
<CartProvider>
<Layout />
</CartProvider>
</ThemeProvider>
);
}
function ThemeToggle() {
const { theme, setTheme } = useTheme();
// Only re-renders when theme changes ✅
}
Solution 2: Memoize Context Value
Copy
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// Memoize the value object
const value = useMemo(() => ({
theme,
toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light')
}), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
Solution 3: State and Dispatch Separation
Copy
const StateContext = createContext();
const DispatchContext = createContext();
function AppProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
// Components that only dispatch don't re-render on state changes
function AddButton() {
const dispatch = useContext(DispatchContext);
return <button onClick={() => dispatch({ type: 'ADD' })}>Add</button>;
}
Context with useReducer
For complex state, combine Context with useReducer:Copy
// contexts/CartContext.jsx
const CartContext = createContext();
const cartReducer = (state, action) => {
switch (action.type) {
case 'ADD_ITEM':
const existing = state.items.find(i => i.id === action.payload.id);
if (existing) {
return {
...state,
items: state.items.map(i =>
i.id === action.payload.id
? { ...i, quantity: i.quantity + 1 }
: i
)
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }]
};
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(i => i.id !== action.payload)
};
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map(i =>
i.id === action.payload.id
? { ...i, quantity: action.payload.quantity }
: i
)
};
case 'CLEAR_CART':
return { ...state, items: [] };
default:
return state;
}
};
export function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
const addItem = (product) => dispatch({ type: 'ADD_ITEM', payload: product });
const removeItem = (id) => dispatch({ type: 'REMOVE_ITEM', payload: id });
const updateQuantity = (id, quantity) => dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
const clearCart = () => dispatch({ type: 'CLEAR_CART' });
const total = state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const itemCount = state.items.reduce(
(sum, item) => sum + item.quantity,
0
);
return (
<CartContext.Provider value={{
items: state.items,
total,
itemCount,
addItem,
removeItem,
updateQuantity,
clearCart
}}>
{children}
</CartContext.Provider>
);
}
export const useCart = () => {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
};
When to Use Context vs. Other Solutions
| Scenario | Recommendation |
|---|---|
| Theme, locale, user data | ✅ Context |
| Data needed by 2-3 components | Props (avoid over-engineering) |
| Complex state with many actions | Context + useReducer or Redux |
| Server state (API data) | React Query / SWR |
| Frequent updates (animations) | Zustand, Jotai, or refs |
| Very large apps | Redux Toolkit, Zustand |
🎯 Practice Exercises
Exercise 1: Notification Context
Exercise 1: Notification Context
Copy
// contexts/NotificationContext.jsx
const NotificationContext = createContext();
export function NotificationProvider({ children }) {
const [notifications, setNotifications] = useState([]);
const addNotification = (message, type = 'info') => {
const id = Date.now();
setNotifications(prev => [...prev, { id, message, type }]);
// Auto-remove after 5 seconds
setTimeout(() => {
removeNotification(id);
}, 5000);
};
const removeNotification = (id) => {
setNotifications(prev => prev.filter(n => n.id !== id));
};
return (
<NotificationContext.Provider value={{ notifications, addNotification, removeNotification }}>
{children}
<NotificationContainer />
</NotificationContext.Provider>
);
}
function NotificationContainer() {
const { notifications, removeNotification } = useNotifications();
return (
<div className="notification-container">
{notifications.map(n => (
<div key={n.id} className={`notification notification-${n.type}`}>
{n.message}
<button onClick={() => removeNotification(n.id)}>×</button>
</div>
))}
</div>
);
}
export const useNotifications = () => useContext(NotificationContext);
// Usage
function SaveButton() {
const { addNotification } = useNotifications();
const handleSave = async () => {
try {
await saveData();
addNotification('Saved successfully!', 'success');
} catch {
addNotification('Failed to save', 'error');
}
};
return <button onClick={handleSave}>Save</button>;
}
Exercise 2: Language/i18n Context
Exercise 2: Language/i18n Context
Copy
// contexts/LanguageContext.jsx
const translations = {
en: {
greeting: 'Hello',
goodbye: 'Goodbye',
welcome: 'Welcome to our app'
},
es: {
greeting: 'Hola',
goodbye: 'Adiós',
welcome: 'Bienvenido a nuestra aplicación'
},
fr: {
greeting: 'Bonjour',
goodbye: 'Au revoir',
welcome: 'Bienvenue dans notre application'
}
};
const LanguageContext = createContext();
export function LanguageProvider({ children }) {
const [language, setLanguage] = useState('en');
const t = (key) => translations[language][key] || key;
return (
<LanguageContext.Provider value={{ language, setLanguage, t }}>
{children}
</LanguageContext.Provider>
);
}
export const useLanguage = () => useContext(LanguageContext);
// Usage
function WelcomeBanner() {
const { t } = useLanguage();
return <h1>{t('welcome')}</h1>;
}
function LanguageSelector() {
const { language, setLanguage } = useLanguage();
return (
<select value={language} onChange={e => setLanguage(e.target.value)}>
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
</select>
);
}
Summary
| Concept | Description |
|---|---|
| Context | Share data without passing props manually |
| createContext | Creates a context object |
| Provider | Component that provides the value |
| useContext | Hook to consume context |
| Custom Hook | Best practice for consuming context |
| Prop Drilling | Problem Context solves |
| Performance | Split contexts to prevent unnecessary re-renders |
| useReducer + Context | Pattern for complex state management |
Next Steps
In the next chapter, you’ll learn about React Router — building multi-page applications with client-side routing!