React Context API - Complete Guide
React Context API provides a way to pass data through the component tree without having to pass props down manually at every level. It’s designed for sharing “global” data like theme, user authentication, or language settings.
What is React Context?
Context is a way to share values between components without explicitly passing props through every level of the component tree. It solves the problem of “prop drilling” - passing props through intermediate components that don’t need them.
When to Use Context
Use Context when:
- You need to share data between many components at different nesting levels
- You have global data like user info, theme, or language
- You want to avoid prop drilling
Don’t use Context just because it’s easier than passing props. Use it when the data is truly global.
Basic Context Usage
1. Creating Context
import { createContext } from 'react';
// Create context with default value
const ThemeContext = createContext('light');
export default ThemeContext;2. Providing Context
import { useState } from 'react';
import ThemeContext from './ThemeContext';
function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<div className={`app ${theme}`}>
<Header />
<Main />
<Footer />
</div>
</ThemeContext.Provider>
);
}3. Consuming Context
import { useContext } from 'react';
import ThemeContext from './ThemeContext';
function Header() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<header className={`header ${theme}`}>
<h1>My App</h1>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
</header>
);
}Complete Context Example
User Authentication Context
// AuthContext.js
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const login = async (email, password) => {
setIsLoading(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// Mock user data
const userData = {
id: 1,
name: 'John Doe',
email: email,
token: 'mock-jwt-token'
};
setUser(userData);
localStorage.setItem('user', JSON.stringify(userData));
} catch (error) {
console.error('Login failed:', error);
} finally {
setIsLoading(false);
}
};
const logout = () => {
setUser(null);
localStorage.removeItem('user');
};
const register = async (name, email, password) => {
setIsLoading(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
const userData = {
id: Date.now(),
name: name,
email: email,
token: 'mock-jwt-token'
};
setUser(userData);
localStorage.setItem('user', JSON.stringify(userData));
} catch (error) {
console.error('Registration failed:', error);
} finally {
setIsLoading(false);
}
};
// Check for existing user on mount
useState(() => {
const savedUser = localStorage.getItem('user');
if (savedUser) {
setUser(JSON.parse(savedUser));
}
});
const value = {
user,
isLoading,
login,
logout,
register,
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;
}Using the Auth Context
// Login.js
import { useState } from 'react';
import { useAuth } from './AuthContext';
function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { login, isLoading } = useAuth();
const handleSubmit = async (e) => {
e.preventDefault();
await login(email, password);
};
return (
<form onSubmit={handleSubmit}>
<h2>Login</h2>
<div>
<label>Email:</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<label>Password:</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</button>
</form>
);
}
// ProtectedRoute.js
import { useAuth } from './AuthContext';
function ProtectedRoute({ children }) {
const { isAuthenticated, user } = useAuth();
if (!isAuthenticated) {
return <div>Please log in to access this page.</div>;
}
return <>{children}</>;
}
// Dashboard.js
import { useAuth } from './AuthContext';
function Dashboard() {
const { user, logout } = useAuth();
return (
<div>
<h1>Welcome, {user.name}!</h1>
<p>Email: {user.email}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
// App.js
import { AuthProvider } from './AuthContext';
function App() {
return (
<AuthProvider>
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />
</Routes>
</Router>
</AuthProvider>
);
}Theme Context Example
// ThemeContext.js
import { createContext, useContext, useState, useEffect } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
useEffect(() => {
// Load theme from localStorage
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
setTheme(savedTheme);
}
}, []);
useEffect(() => {
// Apply theme to document
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
const setThemeMode = (mode) => {
setTheme(mode);
};
const value = {
theme,
toggleTheme,
setThemeMode,
isLight: theme === 'light',
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;
}Using Theme Context
// ThemeButton.js
import { useTheme } from './ThemeContext';
function ThemeButton() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme} className="theme-toggle">
{theme === 'light' ? '🌙' : '☀️'} {theme === 'light' ? 'Dark' : 'Light'} Mode
</button>
);
}
// ThemedComponent.js
import { useTheme } from './ThemeContext';
function ThemedComponent() {
const { theme, isLight, isDark } = useTheme();
return (
<div className={`themed-component ${theme}`}>
<h2>Current Theme: {theme}</h2>
<p>This component adapts to the theme.</p>
{isLight && <p>Light mode is active</p>}
{isDark && <p>Dark mode is active</p>}
<style jsx>{`
.themed-component {
padding: 20px;
border-radius: 8px;
transition: all 0.3s ease;
}
.themed-component.light {
background-color: #ffffff;
color: #333333;
border: 1px solid #e0e0e0;
}
.themed-component.dark {
background-color: #1a1a1a;
color: #ffffff;
border: 1px solid #333333;
}
`}</style>
</div>
);
}Shopping Cart Context Example
// CartContext.js
import { createContext, useContext, useState } from 'react';
const CartContext = createContext(null);
export function CartProvider({ children }) {
const [items, setItems] = useState([]);
const addToCart = (product) => {
setItems(prevItems => {
const existingItem = prevItems.find(item => item.id === product.id);
if (existingItem) {
return prevItems.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prevItems, { ...product, quantity: 1 }];
});
};
const removeFromCart = (productId) => {
setItems(prevItems => prevItems.filter(item => item.id !== productId));
};
const updateQuantity = (productId, quantity) => {
if (quantity <= 0) {
removeFromCart(productId);
return;
}
setItems(prevItems =>
prevItems.map(item =>
item.id === productId
? { ...item, quantity }
: item
)
);
};
const clearCart = () => {
setItems([]);
};
const getTotalPrice = () => {
return items.reduce((total, item) => total + (item.price * item.quantity), 0);
};
const getTotalItems = () => {
return items.reduce((total, item) => total + item.quantity, 0);
};
const value = {
items,
addToCart,
removeFromCart,
updateQuantity,
clearCart,
getTotalPrice,
getTotalItems,
isEmpty: items.length === 0
};
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;
}Using Cart Context
// Product.js
import { useCart } from './CartContext';
function Product({ product }) {
const { addToCart } = useCart();
return (
<div className="product">
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addToCart(product)}>
Add to Cart
</button>
</div>
);
}
// Cart.js
import { useCart } from './CartContext';
function Cart() {
const { items, removeFromCart, updateQuantity, getTotalPrice, isEmpty } = useCart();
if (isEmpty) {
return <div>Your cart is empty</div>;
}
return (
<div className="cart">
<h2>Shopping Cart</h2>
{items.map(item => (
<div key={item.id} className="cart-item">
<span>{item.name}</span>
<span>${item.price}</span>
<div className="quantity-controls">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
>
-
</button>
<span>{item.quantity}</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
>
+
</button>
</div>
<button onClick={() => removeFromCart(item.id)}>
Remove
</button>
</div>
))}
<div className="cart-total">
<strong>Total: ${getTotalPrice().toFixed(2)}</strong>
</div>
</div>
);
}
// CartIcon.js
import { useCart } from './CartContext';
function CartIcon() {
const { getTotalItems } = useCart();
const totalItems = getTotalItems();
return (
<div className="cart-icon">
🛒
{totalItems > 0 && (
<span className="cart-badge">{totalItems}</span>
)}
</div>
);
}Advanced Context Patterns
1. Context with Reducer
// AppContext.js
import { createContext, useContext, useReducer } from 'react';
const initialState = {
user: null,
theme: 'light',
notifications: []
};
function appReducer(state, action) {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'SET_THEME':
return { ...state, theme: action.payload };
case 'ADD_NOTIFICATION':
return {
...state,
notifications: [...state.notifications, action.payload]
};
case 'REMOVE_NOTIFICATION':
return {
...state,
notifications: state.notifications.filter(
notification => notification.id !== action.payload
)
};
case 'LOGOUT':
return { ...state, user: null };
default:
return state;
}
}
const AppContext = createContext(null);
export function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, initialState);
const value = {
...state,
dispatch,
setUser: (user) => dispatch({ type: 'SET_USER', payload: user }),
setTheme: (theme) => dispatch({ type: 'SET_THEME', payload: theme }),
addNotification: (notification) => dispatch({
type: 'ADD_NOTIFICATION',
payload: { ...notification, id: Date.now() }
}),
removeNotification: (id) => dispatch({
type: 'REMOVE_NOTIFICATION',
payload: id
}),
logout: () => dispatch({ type: 'LOGOUT' })
};
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}
export function useApp() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp must be used within an AppProvider');
}
return context;
}2. Multiple Contexts
// App.js
import { AuthProvider } from './AuthContext';
import { ThemeProvider } from './ThemeContext';
import { CartProvider } from './CartContext';
function App() {
return (
<AuthProvider>
<ThemeProvider>
<CartProvider>
<Router>
<Routes>
{/* Your routes here */}
</Routes>
</Router>
</CartProvider>
</ThemeProvider>
</AuthProvider>
);
}3. Context Selectors
// UserContext.js
import { createContext, useContext } from 'react';
const UserContext = createContext(null);
export function UserProvider({ children }) {
const [user, setUser] = useState(null);
const [preferences, setPreferences] = useState({});
const value = {
user,
setUser,
preferences,
setPreferences
};
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}
// Custom hooks for specific context values
export function useUser() {
const { user, setUser } = useContext(UserContext);
return { user, setUser };
}
export function useUserPreferences() {
const { preferences, setPreferences } = useContext(UserContext);
return { preferences, setPreferences };
}Performance Considerations
1. Memoizing Context Value
import { useMemo } from 'react';
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// Memoize context value to prevent unnecessary re-renders
const value = useMemo(() => ({
theme,
toggleTheme: () => setTheme(prev => prev === 'light' ? 'dark' : 'light')
}), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}2. Splitting Context
// Instead of one large context, split into smaller ones
const UserContext = createContext(null);
const ThemeContext = createContext(null);
const NotificationContext = createContext(null);
// Components only subscribe to the contexts they need
function UserProfile() {
const { user } = useContext(UserContext); // Only re-renders when user changes
return <div>{user.name}</div>;
}
function ThemeToggle() {
const { theme, toggleTheme } = useContext(ThemeContext); // Only re-renders when theme changes
return <button onClick={toggleTheme}>Toggle Theme</button>;
}Best Practices
1. Create Custom Hooks
// Good - custom hook for context
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
// Usage
function MyComponent() {
const { user, login } = useAuth(); // Clean and type-safe
}2. Provide Default Values
// Good - provide meaningful default values
const ThemeContext = createContext({
theme: 'light',
toggleTheme: () => {}
});3. Don’t Overuse Context
// Bad - using context for component-specific state
const ButtonContext = createContext(null);
// Good - using props for component-specific state
function Button({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
}Summary
React Context API is powerful for sharing global data:
- Use Context for global state like auth, theme, language
- Create custom hooks for clean context consumption
- Provide meaningful default values
- Split large contexts into smaller ones for better performance
- Memoize context values to prevent unnecessary re-renders
- Don’t use Context for component-specific state
- Always handle the case when context is undefined
Mastering Context API will help you build scalable React applications with clean state management and reduced prop drilling.
External Resources:
Related Tutorials: