useContext - Global State Management

The useContext hook provides a way to pass data through the component tree without having to pass props down manually at every level. This solves the “prop drilling” problem and makes state management much cleaner.

Basic Context Setup

First, create a context and provider:

ThemeContext.js
import React, { createContext, useContext, useState } from 'react';
// Create the context
const ThemeContext = createContext();
// Create a provider component
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
const value = {
theme,
toggleTheme
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook to use the context
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

Using the Context

Wrap your app with the provider and use the context in components:

App.js
import React from 'react';
import { ThemeProvider } from './ThemeContext';
import Header from './Header';
import MainContent from './MainContent';
function App() {
return (
<ThemeProvider>
<div className="app">
<Header />
<MainContent />
</div>
</ThemeProvider>
);
}
// Header.js
import React from 'react';
import { useTheme } from './ThemeContext';
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={`header ${theme}`}>
<h1>My App</h1>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
</header>
);
}
// MainContent.js
import React from 'react';
import { useTheme } from './ThemeContext';
function MainContent() {
const { theme } = useTheme();
return (
<main className={`main-content ${theme}`}>
<p>Current theme: {theme}</p>
</main>
);
}

Advanced Context Patterns

User Authentication Context

AuthContext.js
import React, { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check if user is logged in on app start
const token = localStorage.getItem('authToken');
if (token) {
fetchUserProfile(token)
.then(setUser)
.catch(() => localStorage.removeItem('authToken'))
.finally(() => setLoading(false));
} else {
setLoading(false);
}
}, []);
const login = async (email, password) => {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (response.ok) {
const { user, token } = await response.json();
localStorage.setItem('authToken', token);
setUser(user);
return user;
} else {
throw new Error('Login failed');
}
};
const logout = () => {
localStorage.removeItem('authToken');
setUser(null);
};
const value = {
user,
login,
logout,
loading,
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;
}
async function fetchUserProfile(token) {
const response = await fetch('/api/profile', {
headers: { Authorization: `Bearer ${token}` }
});
return response.json();
}

Shopping Cart Context

CartContext.js
import React, { createContext, useContext, useReducer } from 'react';
const CartContext = createContext();
// Cart reducer
function 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
)
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }]
};
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload.id)
};
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
).filter(item => item.quantity > 0)
};
case 'CLEAR_CART':
return { ...state, items: [] };
default:
return state;
}
}
const initialState = {
items: []
};
export function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, initialState);
const addItem = (product) => {
dispatch({ type: 'ADD_ITEM', payload: product });
};
const removeItem = (productId) => {
dispatch({ type: 'REMOVE_ITEM', payload: { id: productId } });
};
const updateQuantity = (productId, quantity) => {
dispatch({
type: 'UPDATE_QUANTITY',
payload: { id: productId, quantity }
});
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
const getTotalPrice = () => {
return state.items.reduce((total, item) => {
return total + (item.price * item.quantity);
}, 0);
};
const getTotalItems = () => {
return state.items.reduce((total, item) => total + item.quantity, 0);
};
const value = {
items: state.items,
addItem,
removeItem,
updateQuantity,
clearCart,
totalPrice: getTotalPrice(),
totalItems: getTotalItems()
};
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;
}

Multiple Contexts

You can use multiple contexts in the same app:

App.js
function App() {
return (
<AuthProvider>
<ThemeProvider>
<CartProvider>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/profile" element={<Profile />} />
<Route path="/cart" element={<Cart />} />
</Routes>
</Router>
</CartProvider>
</ThemeProvider>
</AuthProvider>
);
}
// Any component can now use all contexts
function Header() {
const { user, logout } = useAuth();
const { theme, toggleTheme } = useTheme();
const { totalItems } = useCart();
return (
<header className={`header ${theme}`}>
<h1>My Store</h1>
<div className="header-actions">
<button onClick={toggleTheme}>
{theme === 'light' ? '🌙' : '☀️'}
</button>
<CartIcon count={totalItems} />
{user ? (
<div>
<span>Hello, {user.name}!</span>
<button onClick={logout}>Logout</button>
</div>
) : (
<LoginButton />
)}
</div>
</header>
);
}

Performance Considerations

Split Contexts for Better Performance

// Instead of one large context
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [cart, setCart] = useState([]);
// When any value changes, ALL consumers re-render
const value = { user, setUser, theme, setTheme, cart, setCart };
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}
// Split into separate contexts
const UserContext = createContext();
const ThemeContext = createContext();
const CartContext = createContext();
// Now components only re-render when their specific context changes

Memoize Context Values

function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = useCallback(() => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
}, []);
// Memoize the context value to prevent unnecessary re-renders
const value = useMemo(() => ({
theme,
toggleTheme
}), [theme, toggleTheme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}

Best Practices

1. Create Custom Hooks

Always create custom hooks for your contexts:

// ✅ Good - Custom hook with error checking
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// ❌ Avoid - Direct useContext usage
function MyComponent() {
const theme = useContext(ThemeContext); // No error checking
// ...
}

2. Keep Contexts Focused

// ✅ Good - Focused contexts
const UserContext = createContext(); // Only user-related state
const ThemeContext = createContext(); // Only theme-related state
// ❌ Avoid - Kitchen sink context
const AppContext = createContext(); // Everything mixed together

3. Provide Default Values

const ThemeContext = createContext({
theme: 'light',
toggleTheme: () => {}
});

Common Mistakes

1. Overusing Context

// ❌ Don't use context for everything
function ParentComponent() {
const [count, setCount] = useState(0);
return (
<CountContext.Provider value={{ count, setCount }}>
<ChildComponent />
</CountContext.Provider>
);
}
// ✅ Use props for simple parent-child communication
function ParentComponent() {
const [count, setCount] = useState(0);
return <ChildComponent count={count} setCount={setCount} />;
}

2. Not Memoizing Context Values

// ❌ This creates a new object on every render
function Provider({ children }) {
const [user, setUser] = useState(null);
return (
<Context.Provider value={{ user, setUser }}>
{children}
</Context.Provider>
);
}
// ✅ Memoize the value
function Provider({ children }) {
const [user, setUser] = useState(null);
const value = useMemo(() => ({ user, setUser }), [user]);
return (
<Context.Provider value={value}>
{children}
</Context.Provider>
);
}

Next Steps

Context is perfect for global state that many components need to access. For more complex state logic, we’ll next explore useReducer, which works great with context for managing complex application state.

The combination of useContext and useReducer is a powerful pattern for state management in React applications!

Share Feedback