Context API
Learn React Context API for sharing state across components without prop drilling. This is a foundational concept in component-based UI development that professional developers rely on daily. The explanations below are written to be beginner-friendly while covering the depth and nuance that comes from real-world React experience. Take your time with each section and practice the examples
What is Context API?
Context API is a React feature that allows you to share state across components without having to explicitly pass props through every level of the component tree.. This is an essential concept that every React developer must understand thoroughly. In professional development environments, getting this right can mean the difference between code that works reliably and code that breaks in production. The following sections break this down into clear, digestible pieces with practical examples you can try immediately
When to Use Context
- Sharing global state across many components — a critical concept in component-based UI development that you will use frequently in real projects
- Avoiding prop drilling — a critical concept in component-based UI development that you will use frequently in real projects
- Theme or language preferences — a critical concept in component-based UI development that you will use frequently in real projects
- User authentication state — a critical concept in component-based UI development that you will use frequently in real projects
- Application-wide settings — a critical concept in component-based UI development that you will use frequently in real projects
Basic Context Setup
import React, { createContext, useContext, useState } from 'react';
// Create a context
const ThemeContext = createContext();
// Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook to use the context
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// Using the context in components
function App() {
return (
<ThemeProvider>
<Header />
<Main />
</ThemeProvider>
);
}
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={theme}>
<h1>My App</h1>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'Dark' : 'Light'}
</button>
</header>
);
}
// Advanced Context with multiple values
const UserContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const login = async (credentials) => {
setLoading(true);
try {
// Simulate API call
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
const userData = await response.json();
setUser(userData);
} catch (error) {
throw error;
} finally {
setLoading(false);
}
};
const logout = () => {
setUser(null);
};
return (
<UserContext.Provider value={{ user, loading, login, logout }}>
{children}
</UserContext.Provider>
);
}
function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
// Using multiple contexts
function App() {
return (
<ThemeProvider>
<UserProvider>
<Header />
<Main />
</UserProvider>
</ThemeProvider>
);
}
function Header() {
const { theme, toggleTheme } = useTheme();
const { user, logout } = useUser();
return (
<header className={theme}>
<h1>My App</h1>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'Dark' : 'Light'}
</button>
{user && (
<div>
<span>Welcome, {user.name}</span>
<button onClick={logout}>Logout</button>
</div>
)}
</header>
);
}Practice Exercise: Context API E-commerce
// Exercise: Build an E-commerce App with Context API
// Create a shopping cart system using Context API
// src/contexts/CartContext.js
import React, { createContext, useContext, useReducer } from 'react';
const CartContext = createContext();
const 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
)
};
} else {
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }]
};
}
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload)
};
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
)
};
case 'CLEAR_CART':
return {
...state,
items: []
};
default:
return state;
}
};
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, {
items: [],
total: 0
});
const addItem = (item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
const removeItem = (itemId) => {
dispatch({ type: 'REMOVE_ITEM', payload: itemId });
};
const updateQuantity = (itemId, quantity) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id: itemId, quantity } });
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
const getTotal = () => {
return state.items.reduce((total, item) => total + (item.price * item.quantity), 0);
};
const getItemCount = () => {
return state.items.reduce((count, item) => count + item.quantity, 0);
};
return (
<CartContext.Provider value={{
items: state.items,
addItem,
removeItem,
updateQuantity,
clearCart,
getTotal,
getItemCount
}}>
{children}
</CartContext.Provider>
);
}
function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
}
// src/components/ProductCard.js
function ProductCard({ product }) {
const { addItem } = useCart();
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.price}</p>
<button onClick={() => addItem(product)}>
Add to Cart
</button>
</div>
);
}
// src/components/Cart.js
function Cart() {
const { items, removeItem, updateQuantity, getTotal, clearCart } = useCart();
if (items.length === 0) {
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">
<img src={item.image} alt={item.name} />
<div>
<h3>{item.name}</h3>
<p>{item.price}</p>
<input
type="number"
value={item.quantity}
onChange={(e) => updateQuantity(item.id, parseInt(e.target.value))}
min="1"
/>
<button onClick={() => removeItem(item.id)}>
Remove
</button>
</div>
</div>
))}
<div className="cart-total">
<strong>Total: {getTotal()}</strong>
</div>
<button onClick={clearCart}>Clear Cart</button>
</div>
);
}
// src/components/CartIcon.js
function CartIcon() {
const { getItemCount } = useCart();
const itemCount = getItemCount();
return (
<div className="cart-icon">
🛒
{itemCount > 0 && (
<span className="cart-count">{itemCount}</span>
)}
</div>
);
}
// src/App.js
function App() {
return (
<CartProvider>
<div className="app">
<header>
<h1>E-commerce Store</h1>
<CartIcon />
</header>
<main>
<ProductList />
<Cart />
</main>
</div>
</CartProvider>
);
}
// Challenge: Add a wishlist feature
// Challenge: Add product categories and filtering
// Challenge: Add user authentication context