useContext - Global State Management
The useContext hook provides a way to pass data through the component tree without having to pass props down manually at every level. This solves the “prop drilling” problem and makes state management much cleaner.
Basic Context Setup
First, create a context and provider:
import React, { createContext, useContext, useState } from 'react';
// Create the contextconst ThemeContext = createContext();
// Create a provider componentexport function ThemeProvider({ children }) { const [theme, setTheme] = useState('light');
const toggleTheme = () => { setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light'); };
const value = { theme, toggleTheme };
return ( <ThemeContext.Provider value={value}> {children} </ThemeContext.Provider> );}
// Custom hook to use the contextexport function useTheme() { const context = useContext(ThemeContext);
if (!context) { throw new Error('useTheme must be used within a ThemeProvider'); }
return context;}Using the Context
Wrap your app with the provider and use the context in components:
import React from 'react';import { ThemeProvider } from './ThemeContext';import Header from './Header';import MainContent from './MainContent';
function App() { return ( <ThemeProvider> <div className="app"> <Header /> <MainContent /> </div> </ThemeProvider> );}
// Header.jsimport React from 'react';import { useTheme } from './ThemeContext';
function Header() { const { theme, toggleTheme } = useTheme();
return ( <header className={`header ${theme}`}> <h1>My App</h1> <button onClick={toggleTheme}> Switch to {theme === 'light' ? 'dark' : 'light'} mode </button> </header> );}
// MainContent.jsimport React from 'react';import { useTheme } from './ThemeContext';
function MainContent() { const { theme } = useTheme();
return ( <main className={`main-content ${theme}`}> <p>Current theme: {theme}</p> </main> );}Advanced Context Patterns
User Authentication Context
import React, { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true);
useEffect(() => { // Check if user is logged in on app start const token = localStorage.getItem('authToken'); if (token) { fetchUserProfile(token) .then(setUser) .catch(() => localStorage.removeItem('authToken')) .finally(() => setLoading(false)); } else { setLoading(false); } }, []);
const login = async (email, password) => { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) });
if (response.ok) { const { user, token } = await response.json(); localStorage.setItem('authToken', token); setUser(user); return user; } else { throw new Error('Login failed'); } };
const logout = () => { localStorage.removeItem('authToken'); setUser(null); };
const value = { user, login, logout, loading, isAuthenticated: !!user };
return ( <AuthContext.Provider value={value}> {children} </AuthContext.Provider> );}
export function useAuth() { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within an AuthProvider'); } return context;}
async function fetchUserProfile(token) { const response = await fetch('/api/profile', { headers: { Authorization: `Bearer ${token}` } }); return response.json();}Shopping Cart Context
import React, { createContext, useContext, useReducer } from 'react';
const CartContext = createContext();
// Cart reducerfunction cartReducer(state, action) { switch (action.type) { case 'ADD_ITEM': const existingItem = state.items.find(item => item.id === action.payload.id);
if (existingItem) { return { ...state, items: state.items.map(item => item.id === action.payload.id ? { ...item, quantity: item.quantity + 1 } : item ) }; }
return { ...state, items: [...state.items, { ...action.payload, quantity: 1 }] };
case 'REMOVE_ITEM': return { ...state, items: state.items.filter(item => item.id !== action.payload.id) };
case 'UPDATE_QUANTITY': return { ...state, items: state.items.map(item => item.id === action.payload.id ? { ...item, quantity: action.payload.quantity } : item ).filter(item => item.quantity > 0) };
case 'CLEAR_CART': return { ...state, items: [] };
default: return state; }}
const initialState = { items: []};
export function CartProvider({ children }) { const [state, dispatch] = useReducer(cartReducer, initialState);
const addItem = (product) => { dispatch({ type: 'ADD_ITEM', payload: product }); };
const removeItem = (productId) => { dispatch({ type: 'REMOVE_ITEM', payload: { id: productId } }); };
const updateQuantity = (productId, quantity) => { dispatch({ type: 'UPDATE_QUANTITY', payload: { id: productId, quantity } }); };
const clearCart = () => { dispatch({ type: 'CLEAR_CART' }); };
const getTotalPrice = () => { return state.items.reduce((total, item) => { return total + (item.price * item.quantity); }, 0); };
const getTotalItems = () => { return state.items.reduce((total, item) => total + item.quantity, 0); };
const value = { items: state.items, addItem, removeItem, updateQuantity, clearCart, totalPrice: getTotalPrice(), totalItems: getTotalItems() };
return ( <CartContext.Provider value={value}> {children} </CartContext.Provider> );}
export function useCart() { const context = useContext(CartContext); if (!context) { throw new Error('useCart must be used within a CartProvider'); } return context;}Multiple Contexts
You can use multiple contexts in the same app:
function App() { return ( <AuthProvider> <ThemeProvider> <CartProvider> <Router> <Routes> <Route path="/" element={<Home />} /> <Route path="/profile" element={<Profile />} /> <Route path="/cart" element={<Cart />} /> </Routes> </Router> </CartProvider> </ThemeProvider> </AuthProvider> );}
// Any component can now use all contextsfunction Header() { const { user, logout } = useAuth(); const { theme, toggleTheme } = useTheme(); const { totalItems } = useCart();
return ( <header className={`header ${theme}`}> <h1>My Store</h1>
<div className="header-actions"> <button onClick={toggleTheme}> {theme === 'light' ? '🌙' : '☀️'} </button>
<CartIcon count={totalItems} />
{user ? ( <div> <span>Hello, {user.name}!</span> <button onClick={logout}>Logout</button> </div> ) : ( <LoginButton /> )} </div> </header> );}Performance Considerations
Split Contexts for Better Performance
// Instead of one large contextconst AppContext = createContext();
function AppProvider({ children }) { const [user, setUser] = useState(null); const [theme, setTheme] = useState('light'); const [cart, setCart] = useState([]);
// When any value changes, ALL consumers re-render const value = { user, setUser, theme, setTheme, cart, setCart };
return ( <AppContext.Provider value={value}> {children} </AppContext.Provider> );}
// Split into separate contextsconst UserContext = createContext();const ThemeContext = createContext();const CartContext = createContext();
// Now components only re-render when their specific context changesMemoize Context Values
function ThemeProvider({ children }) { const [theme, setTheme] = useState('light');
const toggleTheme = useCallback(() => { setTheme(prev => prev === 'light' ? 'dark' : 'light'); }, []);
// Memoize the context value to prevent unnecessary re-renders const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
return ( <ThemeContext.Provider value={value}> {children} </ThemeContext.Provider> );}Best Practices
1. Create Custom Hooks
Always create custom hooks for your contexts:
// ✅ Good - Custom hook with error checkingexport function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error('useTheme must be used within a ThemeProvider'); } return context;}
// ❌ Avoid - Direct useContext usagefunction MyComponent() { const theme = useContext(ThemeContext); // No error checking // ...}2. Keep Contexts Focused
// ✅ Good - Focused contextsconst UserContext = createContext(); // Only user-related stateconst ThemeContext = createContext(); // Only theme-related state
// ❌ Avoid - Kitchen sink contextconst AppContext = createContext(); // Everything mixed together3. Provide Default Values
const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {}});Common Mistakes
1. Overusing Context
// ❌ Don't use context for everythingfunction ParentComponent() { const [count, setCount] = useState(0);
return ( <CountContext.Provider value={{ count, setCount }}> <ChildComponent /> </CountContext.Provider> );}
// ✅ Use props for simple parent-child communicationfunction ParentComponent() { const [count, setCount] = useState(0);
return <ChildComponent count={count} setCount={setCount} />;}2. Not Memoizing Context Values
// ❌ This creates a new object on every renderfunction Provider({ children }) { const [user, setUser] = useState(null);
return ( <Context.Provider value={{ user, setUser }}> {children} </Context.Provider> );}
// ✅ Memoize the valuefunction Provider({ children }) { const [user, setUser] = useState(null);
const value = useMemo(() => ({ user, setUser }), [user]);
return ( <Context.Provider value={value}> {children} </Context.Provider> );}Next Steps
Context is perfect for global state that many components need to access. For more complex state logic, we’ll next explore useReducer, which works great with context for managing complex application state.
The combination of useContext and useReducer is a powerful pattern for state management in React applications!