Context API
useContext lets components read shared data. The Context API pattern turns that into a tidy module: a provider that owns the state and a custom hook that exposes it.
On this page · 6 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Write a provider component that owns state
- Expose context through a custom hook with a safety check
- Combine context with useReducer
- Split contexts to avoid unnecessary re-renders
Explanation#
Pattern: one file per context. It creates the context, exports a Provider component that holds state, and exports a useX() hook for consumers.
The hook can throw a clear error when used outside the provider — far easier to debug than a silent default value.
For complex updates, the provider uses useReducer and exposes dispatch or named actions (addItem, removeItem).
Every consumer re-renders when the context value changes. Split state and actions into two contexts, or split unrelated data (auth vs cart), so components only re-render for what they use.
Context is not a replacement for all state. Keep local state local; use context for things many distant components need.
Syntax#
const CartContext = createContext(null);
export function CartProvider({ children }) {
const [items, setItems] = useState([]);
return <CartContext value={{ items, setItems }}>{children}</CartContext>;
}
export function useCart() {
const ctx = useContext(CartContext);
if (!ctx) throw new Error('useCart must be used inside <CartProvider>');
return ctx;
}
Code example: A cart context with reducer and custom hook#
import { createContext, useContext, useMemo, useReducer } from 'react';
const CartContext = createContext(null);
function cartReducer(items, action) {
switch (action.type) {
case 'add': {
const found = items.find(i => i.id === action.product.id);
return found
? items.map(i => (i.id === found.id ? { ...i, qty: i.qty + 1 } : i))
: [...items, { ...action.product, qty: 1 }];
}
case 'remove':
return items.filter(i => i.id !== action.id);
default:
throw new Error('Unknown action ' + action.type);
}
}
export function CartProvider({ children }) {
const [items, dispatch] = useReducer(cartReducer, []);
const value = useMemo(() => ({
items,
count: items.reduce((n, i) => n + i.qty, 0),
total: items.reduce((s, i) => s + i.qty * i.price, 0),
add: product => dispatch({ type: 'add', product }),
remove: id => dispatch({ type: 'remove', id }),
}), [items]);
return <CartContext value={value}>{children}</CartContext>;
}
export function useCart() {
const ctx = useContext(CartContext);
if (!ctx) throw new Error('useCart must be used inside <CartProvider>');
return ctx;
}
// ── Consumers anywhere in the tree ──
function CartBadge() {
const { count, total } = useCart();
return <p>🛒 {count} items · ₹{total}</p>;
}
function ProductButton({ product }) {
const { add } = useCart();
return <button onClick={() => add(product)}>Add {product.name}</button>;
}
export default function App() {
return (
<CartProvider>
<CartBadge />
<ProductButton product={{ id: 1, name: 'Pen', price: 20 }} />
<ProductButton product={{ id: 2, name: 'Bag', price: 650 }} />
</CartProvider>
);
}
The badge starts at “🛒 0 items · ₹0”. Clicking “Add Pen” twice and “Add Bag” once makes it “🛒 3 items · ₹690”. The buttons and the badge never pass props to each other — they share the cart through the provider.
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- CartProvider owns the state with useReducer and builds the value object.
- count and total are derived inside the provider so every consumer gets the same numbers.
- useMemo keeps the value object stable unless items change.
- useCart gives consumers the cart and throws a helpful error outside the provider.
- CartBadge and ProductButton just call useCart().
Real-world use case#
An e-commerce front end has AuthProvider, CartProvider and ThemeProvider wrapping the router in main.jsx. Product pages, the header and checkout all use useAuth() and useCart().
Common mistakes#
Without the check, consumers silently get null and crash later. Throw a clear error in the hook.
Putting user, cart, theme and notifications in one value re-renders everything on any change. Split by concern.
Consumers depend on its shape. Export a hook and keep the context private to the file.
Best practices#
- One file per context: provider + hook
- Throw when used outside the provider
- Derive values inside the provider
- Split contexts by concern
Remember this#
Provider owns state; hook exposes it.
useX() outside <XProvider> → clear error.
Many unrelated values → many small contexts.
Practice exercise#
Your task: Write an AuthProvider with user (null by default), login(name) and logout(), plus a useAuth hook. Show "Hi, Asha" with a Logout button, or a Login button.
Need a hint?
Show solution
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const value = { user, login: name => setUser({ name }), logout: () => setUser(null) };
return <AuthContext value={value}>{children}</AuthContext>;
}
function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used inside <AuthProvider>');
return ctx;
}
function UserMenu() {
const { user, login, logout } = useAuth();
return user
? <p>Hi, {user.name} <button onClick={logout}>Logout</button></p>
: <button onClick={() => login('Asha')}>Login</button>;
}
export default function App() {
return <AuthProvider><UserMenu /></AuthProvider>;
}
Mini quiz#
Why export a useCart() hook instead of the CartContext object?
Want more? Practise all React MCQs · React interview questions
My notes#
Saved in this browser.