useContext Hook
useContext Hook
The useContext Hook provides a way to pass data through the component tree without having to pass props down manually at every level. It's React's solution to "prop drilling."
Understanding Context
Context provides a way to share values between components without explicitly passing props through every level of the tree.
Creating Context
import React, { createContext, useContext } from 'react';
// Create a context
const ThemeContext = createContext();
// Create a provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
Using Context
// Using useContext Hook
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
>
Current theme: {theme}
</button>
);
}
// App component
function App() {
return (
<ThemeProvider>
<ThemedButton />
</ThemeProvider>
);
}
Basic Example
Without Context (Prop Drilling)
// Problem: Passing props through multiple levels
function App() {
const [user, setUser] = useState({ name: 'John', role: 'admin' });
return <Dashboard user={user} setUser={setUser} />;
}
function Dashboard({ user, setUser }) {
return <Profile user={user} setUser={setUser} />;
}
function Profile({ user, setUser }) {
return <ProfileEditor user={user} setUser={setUser} />;
}
function ProfileEditor({ user, setUser }) {
return <div>Editing {user.name}</div>;
}
With Context
// Solution: Using Context
const UserContext = createContext();
function App() {
const [user, setUser] = useState({ name: 'John', role: 'admin' });
return (
<UserContext.Provider value={{ user, setUser }}>
<Dashboard />
</UserContext.Provider>
);
}
function Dashboard() {
return <Profile />;
}
function Profile() {
return <ProfileEditor />;
}
function ProfileEditor() {
const { user, setUser } = useContext(UserContext);
return <div>Editing {user.name}</div>;
}
Complete Authentication Example
// AuthContext.js
import React, { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check if user is logged in
const token = localStorage.getItem('token');
if (token) {
fetchUser(token)
.then(setUser)
.catch(() => localStorage.removeItem('token'))
.finally(() => setLoading(false));
} else {
setLoading(false);
}
}, []);
const login = async (email, password) => {
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
localStorage.setItem('token', data.token);
setUser(data.user);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
};
const logout = () => {
localStorage.removeItem('token');
setUser(null);
};
const value = {
user,
login,
logout,
loading,
isAuthenticated: !!user
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// Custom hook for using auth context
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
// Components using auth
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { login } = useAuth();
const handleSubmit = async (e) => {
e.preventDefault();
const result = await login(email, password);
if (!result.success) {
alert(result.error);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
}
function UserProfile() {
const { user, logout, isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <div>Please log in</div>;
}
return (
<div>
<h1>Welcome, {user.name}!</h1>
<p>Email: {user.email}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
function App() {
const { loading } = useAuth();
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<UserProfile />
<LoginForm />
</div>
);
}
// Root component
function Root() {
return (
<AuthProvider>
<App />
</AuthProvider>
);
}
Multiple Contexts
You can use multiple contexts in your application:
// Different contexts for different concerns
const ThemeContext = createContext();
const LanguageContext = createContext();
const UserContext = createContext();
function App() {
return (
<ThemeProvider>
<LanguageProvider>
<UserProvider>
<MainContent />
</UserProvider>
</LanguageProvider>
</ThemeProvider>
);
}
// Component using multiple contexts
function Header() {
const { theme } = useContext(ThemeContext);
const { language } = useContext(LanguageContext);
const { user } = useContext(UserContext);
return (
<header className={theme}>
<h1>{translations[language].welcome}, {user.name}!</h1>
</header>
);
}
Shopping Cart Context
const CartContext = createContext();
function CartProvider({ children }) {
const [items, setItems] = useState([]);
const addItem = (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 removeItem = (productId) => {
setItems(prevItems => prevItems.filter(item => item.id !== productId));
};
const updateQuantity = (productId, quantity) => {
if (quantity <= 0) {
removeItem(productId);
return;
}
setItems(prevItems =>
prevItems.map(item =>
item.id === productId ? { ...item, quantity } : item
)
);
};
const clearCart = () => setItems([]);
const totalItems = items.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const value = {
items,
addItem,
removeItem,
updateQuantity,
clearCart,
totalItems,
totalPrice
};
return (
<CartContext.Provider value={value}>
{children}
</CartContext.Provider>
);
}
function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within CartProvider');
}
return context;
}
// Using the cart
function ProductCard({ product }) {
const { addItem } = useCart();
return (
<div>
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addItem(product)}>Add to Cart</button>
</div>
);
}
function CartSummary() {
const { items, totalItems, totalPrice, removeItem } = useCart();
return (
<div>
<h2>Cart ({totalItems} items)</h2>
{items.map(item => (
<div key={item.id}>
<span>{item.name} x {item.quantity}</span>
<span>${item.price * item.quantity}</span>
<button onClick={() => removeItem(item.id)}>Remove</button>
</div>
))}
<h3>Total: ${totalPrice.toFixed(2)}</h3>
</div>
);
}
Performance Optimization
Context Value Optimization
// ❌ Bad - Creates new object on every render
function BadProvider({ children }) {
const [state, setState] = useState();
return (
<Context.Provider value={{ state, setState }}>
{children}
</Context.Provider>
);
}
// ✅ Good - Memoize the value
function GoodProvider({ children }) {
const [state, setState] = useState();
const value = useMemo(
() => ({ state, setState }),
[state]
);
return (
<Context.Provider value={value}>
{children}
</Context.Provider>
);
}
Splitting Contexts
// Instead of one large context
const AppContext = createContext();
// Split into multiple focused contexts
const UserContext = createContext();
const ThemeContext = createContext();
const SettingsContext = createContext();
// This way, components only re-render when their specific context changes
Context with Reducer
Combining useContext with useReducer for complex state:
const StateContext = createContext();
const DispatchContext = createContext();
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'SET_USER':
return { ...state, user: action.payload };
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function AppProvider({ children }) {
const [state, dispatch] = useReducer(reducer, {
count: 0,
user: null
});
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
function useAppState() {
const context = useContext(StateContext);
if (!context) {
throw new Error('useAppState must be used within AppProvider');
}
return context;
}
function useAppDispatch() {
const context = useContext(DispatchContext);
if (!context) {
throw new Error('useAppDispatch must be used within AppProvider');
}
return context;
}
// Using the context
function Counter() {
const { count } = useAppState();
const dispatch = useAppDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
</div>
);
}
Best Practices
- Don't overuse context - Not everything needs to be in context
- Split contexts by concern to minimize re-renders
- Memoize context values to prevent unnecessary re-renders
- Create custom hooks for consuming context
- Provide good default values or handle null cases
- Keep context close to where it's used
- Document context shape with TypeScript or comments
When to Use Context
Good Use Cases:
- Theme/appearance settings
- User authentication state
- Language/locale preferences
- Shopping cart data
- App-wide configuration
When NOT to Use Context:
- Frequently changing values (causes many re-renders)
- Very local state (just lift state up instead)
- Complex state with many consumers (consider state management library)
Common Patterns
Protected Routes
function ProtectedRoute({ children }) {
const { isAuthenticated } = useAuth();
const navigate = useNavigate();
useEffect(() => {
if (!isAuthenticated) {
navigate('/login');
}
}, [isAuthenticated, navigate]);
return isAuthenticated ? children : null;
}
Feature Flags
const FeatureFlagsContext = createContext();
function FeatureFlagsProvider({ children }) {
const [flags, setFlags] = useState({
newUI: false,
betaFeatures: false,
darkMode: true
});
return (
<FeatureFlagsContext.Provider value={flags}>
{children}
</FeatureFlagsContext.Provider>
);
}
function useFeatureFlag(flagName) {
const flags = useContext(FeatureFlagsContext);
return flags[flagName] ?? false;
}
// Usage
function NewFeature() {
const isEnabled = useFeatureFlag('betaFeatures');
if (!isEnabled) return null;
return <div>Beta Feature!</div>;
}
useContext is a powerful tool for avoiding prop drilling and sharing state across your React application. Use it wisely to keep your code clean and maintainable!