What is Context?
Context provides a way to pass data through the component tree without having to pass props down manually at every level. Perfect for global data like themes, user info, language settings.
The Problem: Prop Drilling
// Passing props through many levels
<App user={user}>
<Dashboard user={user}>
<Sidebar user={user}>
<UserMenu user={user} />
</Sidebar>
</Dashboard>
</App>
// Solution: Context!
// UserMenu can access user directly
📚 Basic Setup
Step 1: Create Context
import { createContext } from 'react';
// Create context with default value
const ThemeContext = createContext('light');
Step 2: Provide Context
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
Step 3: Consume Context
import { useContext } from 'react';
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button className={`btn-${theme}`}>
I'm {theme} themed!
</button>
);
}
🎨 Complete Theme Example
// ThemeContext.js
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook for cleaner usage
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// App.js
function App() {
return (
<ThemeProvider>
<Header />
<Main />
<Footer />
</ThemeProvider>
);
}
// Any component can use theme
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={theme}>
<h1>My App</h1>
<button onClick={toggleTheme}>
Toggle {theme === 'light' ? '🌙' : '☀️'}
</button>
</header>
);
}
👤 User Authentication Context
// AuthContext.js
import { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// Check if user is logged in on mount
useEffect(() => {
const checkAuth = async () => {
try {
const response = await fetch('/api/me');
const data = await response.json();
setUser(data);
} catch (error) {
setUser(null);
} finally {
setLoading(false);
}
};
checkAuth();
}, []);
const login = async (email, password) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
const data = await response.json();
setUser(data.user);
return data;
};
const logout = async () => {
await fetch('/api/logout', { method: 'POST' });
setUser(null);
};
const value = {
user,
loading,
login,
logout,
isAuthenticated: !!user
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
return useContext(AuthContext);
}
// Usage in components
function Navbar() {
const { user, logout, isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Link to="/login">Login</Link>;
}
return (
<div>
<span>Welcome, {user.name}!</span>
<button onClick={logout}>Logout</button>
</div>
);
}
function ProtectedRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
if (loading) return <div>Loading...</div>;
if (!isAuthenticated) return <Navigate to="/login" />;
return children;
}
🌐 Multiple Contexts
// Combine multiple providers
function App() {
return (
<AuthProvider>
<ThemeProvider>
<LanguageProvider>
<NotificationProvider>
<Router>
<Routes />
</Router>
</NotificationProvider>
</LanguageProvider>
</ThemeProvider>
</AuthProvider>
);
}
// Use multiple contexts in one component
function UserProfile() {
const { user } = useAuth();
const { theme } = useTheme();
const { t } = useLanguage();
const { notify } = useNotification();
return (
<div className={`profile ${theme}`}>
<h2>{t('welcome', { name: user.name })}</h2>
<button onClick={() => notify('Profile updated!')}>
{t('save')}
</button>
</div>
);
}
🛒 Shopping Cart Context
// CartContext.js
const CartContext = createContext();
export function CartProvider({ children }) {
const [items, setItems] = useState([]);
const addToCart = (product) => {
setItems(prev => {
const existing = prev.find(item => item.id === product.id);
if (existing) {
return prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prev, { ...product, quantity: 1 }];
});
};
const removeFromCart = (productId) => {
setItems(prev => prev.filter(item => item.id !== productId));
};
const updateQuantity = (productId, quantity) => {
if (quantity <= 0) {
removeFromCart(productId);
return;
}
setItems(prev =>
prev.map(item =>
item.id === productId ? { ...item, quantity } : item
)
);
};
const clearCart = () => setItems([]);
const total = items.reduce((sum, item) =>
sum + item.price * item.quantity, 0
);
const itemCount = items.reduce((sum, item) =>
sum + item.quantity, 0
);
const value = {
items,
addToCart,
removeFromCart,
updateQuantity,
clearCart,
total,
itemCount
};
return (
<CartContext.Provider value={value}>
{children}
</CartContext.Provider>
);
}
export function useCart() {
return useContext(CartContext);
}
// Usage
function ProductCard({ product }) {
const { addToCart } = useCart();
return (
<div>
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addToCart(product)}>
Add to Cart
</button>
</div>
);
}
function CartIcon() {
const { itemCount } = useCart();
return (
<div>
🛒
{itemCount > 0 && <span className="badge">{itemCount}</span>}
</div>
);
}
function Checkout() {
const { items, total, clearCart } = useCart();
return (
<div>
{items.map(item => (
<div key={item.id}>
{item.name} x {item.quantity} = ${item.price * item.quantity}
</div>
))}
<h3>Total: ${total.toFixed(2)}</h3>
<button onClick={clearCart}>Clear Cart</button>
</div>
);
}
⚡ Performance Optimization
Problem: Unnecessary Re-renders
// ❌ Creates new object every render
function Provider({ children }) {
const [count, setCount] = useState(0);
// New object on every render!
return (
<Context.Provider value={{ count, setCount }}>
{children}
</Context.Provider>
);
}
// ✅ Memoize the value
function Provider({ children }) {
const [count, setCount] = useState(0);
const value = useMemo(
() => ({ count, setCount }),
[count]
);
return (
<Context.Provider value={value}>
{children}
</Context.Provider>
);
}
Split Contexts for Better Performance:
// ❌ One big context
const AppContext = createContext();
// Changes to any value re-render all consumers
// ✅ Split into smaller contexts
const UserContext = createContext();
const ThemeContext = createContext();
const SettingsContext = createContext();
// Only relevant consumers re-render
⚠️ Common Mistakes
1. Using Context Outside Provider
// Error: useContext returns undefined
function Component() {
const value = useContext(MyContext);
// value is undefined if no Provider above
}
// ✅ Always check
export function useMyContext() {
const context = useContext(MyContext);
if (context === undefined) {
throw new Error('useMyContext must be used within Provider');
}
return context;
}
2. Not Memoizing Context Value
// ❌ New object every render
<Context.Provider value={{ user, setUser }}>
// ✅ Memoize
const value = useMemo(() => ({ user, setUser }), [user]);
🎯 Key Takeaways
- Context: Share data without prop drilling
- createContext: Create context with default value
- Provider: Wrap components to provide value
- useContext: Consume context value in components
- Custom hooks: Create useTheme, useAuth, etc.
- Memoize: Use useMemo for context value
- Split contexts: Avoid one giant context
- Not for everything: Use props for local data