useContext & Context API - Global State Management
Understanding Context in React
Context provides a way to pass data through the component tree without having to pass props down manually at every level. It's designed to share data that can be considered "global" for a tree of React components, such as current authenticated user, theme, or preferred language.
The Context API consists of two main parts: a Provider component that supplies the data, and a way to consume that data in child components using either the useContext hook or a Consumer component.
When to Use Context
Use Context when you have data that needs to be accessible by many components at different nesting levels. Common use cases include: theming, user authentication, language preferences, and application-wide settings.
Creating a Context
First, create a context using React.createContext():
import React, { createContext, useState } from 'react';
// Create context with default value
const ThemeContext = createContext('light');
export default ThemeContext;
Providing Context Value
Wrap your component tree with a Provider to make the context value available to all child components:
import React, { useState } from 'react';
import ThemeContext from './ThemeContext';
function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<div className={`app ${theme}`}>
<Header />
<MainContent />
<Footer />
</div>
</ThemeContext.Provider>
);
}
export default App;
Consuming Context with useContext
The useContext hook is the easiest way to access context values in function components:
import React, { useContext } from 'react';
import ThemeContext from './ThemeContext';
function Header() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<header className={`header ${theme}`}>
<h1>My Application</h1>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
</button>
</header>
);
}
export default Header;
Context Provider Pattern
Create a custom provider component that encapsulates context creation, state management, and provider logic. This keeps your code organized and makes the context easier to use.
Complete Theme Context Example
Here's a full example with a custom provider:
// ThemeContext.js
import React, { createContext, useState, useContext } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
const value = {
theme,
toggleTheme
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook to use theme context
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// App.js
import React from 'react';
import { ThemeProvider } from './ThemeContext';
import Header from './Header';
import Content from './Content';
function App() {
return (
<ThemeProvider>
<div className="app">
<Header />
<Content />
</div>
</ThemeProvider>
);
}
export default App;
// Header.js
import React from 'react';
import { useTheme } from './ThemeContext';
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff'
}}>
<h1>Theme Example</h1>
<button onClick={toggleTheme}>
Toggle Theme
</button>
</header>
);
}
export default Header;
Authentication Context Example
A common use case is managing user authentication state:
// AuthContext.js
import React, { createContext, useState, useContext } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const login = async (email, password) => {
setLoading(true);
try {
// Simulate API call
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
headers: { 'Content-Type': 'application/json' }
});
const data = await response.json();
setUser(data.user);
localStorage.setItem('token', data.token);
} catch (error) {
console.error('Login failed:', error);
throw error;
} finally {
setLoading(false);
}
};
const logout = () => {
setUser(null);
localStorage.removeItem('token');
};
const value = {
user,
loading,
login,
logout,
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 AuthProvider');
}
return context;
}
// Usage in component
import React from 'react';
import { useAuth } from './AuthContext';
function UserProfile() {
const { user, logout, isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <div>Please log in</div>;
}
return (
<div>
<h2>Welcome, {user.name}!</h2>
<p>Email: {user.email}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
Context Performance Considerations
When a context value changes, all components consuming that context will re-render. To optimize performance, split contexts by concern and use React.memo for expensive child components.
Multiple Contexts
You can nest multiple context providers to use different contexts:
import React from 'react';
import { ThemeProvider } from './ThemeContext';
import { AuthProvider } from './AuthContext';
import { LanguageProvider } from './LanguageContext';
function App() {
return (
<AuthProvider>
<ThemeProvider>
<LanguageProvider>
<MainApp />
</LanguageProvider>
</ThemeProvider>
</AuthProvider>
);
}
// Component using multiple contexts
function Dashboard() {
const { user } = useAuth();
const { theme } = useTheme();
const { language, t } = useLanguage();
return (
<div className={`dashboard ${theme}`}>
<h1>{t('welcome')}, {user.name}!</h1>
<p>Current language: {language}</p>
</div>
);
}
Optimizing Context Updates
Split context values to prevent unnecessary re-renders:
// Instead of one context with all user data
// Split into separate contexts for state and actions
// UserStateContext.js
const UserStateContext = createContext();
const UserActionsContext = createContext();
export function UserProvider({ children }) {
const [user, setUser] = useState(null);
// Actions don't change, so they won't cause re-renders
const actions = useMemo(() => ({
updateUser: (newUser) => setUser(newUser),
clearUser: () => setUser(null)
}), []);
return (
<UserStateContext.Provider value={user}>
<UserActionsContext.Provider value={actions}>
{children}
</UserActionsContext.Provider>
</UserStateContext.Provider>
);
}
export function useUserState() {
return useContext(UserStateContext);
}
export function useUserActions() {
return useContext(UserActionsContext);
}
// Components only re-render when their specific context changes
function UserDisplay() {
const user = useUserState(); // Re-renders when user changes
return <div>{user?.name}</div>;
}
function UserControls() {
const { updateUser } = useUserActions(); // Never re-renders
return <button onClick={() => updateUser({ name: 'John' })}>Update</button>;
}
Language/Localization Context
// LanguageContext.js
import React, { createContext, useState, useContext } from 'react';
const translations = {
en: {
welcome: 'Welcome',
goodbye: 'Goodbye',
hello: 'Hello'
},
ar: {
welcome: 'مرحبا',
goodbye: 'وداعا',
hello: 'أهلا'
}
};
const LanguageContext = createContext();
export function LanguageProvider({ children }) {
const [language, setLanguage] = useState('en');
const t = (key) => {
return translations[language][key] || key;
};
const changeLanguage = (lang) => {
setLanguage(lang);
document.documentElement.lang = lang;
document.documentElement.dir = lang === 'ar' ? 'rtl' : 'ltr';
};
const value = {
language,
changeLanguage,
t
};
return (
<LanguageContext.Provider value={value}>
{children}
</LanguageContext.Provider>
);
}
export function useLanguage() {
const context = useContext(LanguageContext);
if (!context) {
throw new Error('useLanguage must be used within LanguageProvider');
}
return context;
}
// Usage
function Greeting() {
const { t, language, changeLanguage } = useLanguage();
return (
<div>
<h1>{t('hello')}</h1>
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('ar')}>العربية</button>
</div>
);
}
Shopping Cart Context Example
// CartContext.js
import React, { createContext, useContext, useReducer } from 'react';
const CartContext = createContext();
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
const existingIndex = state.items.findIndex(
item => item.id === action.payload.id
);
if (existingIndex >= 0) {
const newItems = [...state.items];
newItems[existingIndex].quantity += 1;
return { ...state, items: newItems };
}
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;
}
}
export function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
const addItem = (item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
const removeItem = (id) => {
dispatch({ type: 'REMOVE_ITEM', payload: id });
};
const updateQuantity = (id, quantity) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
const total = state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const value = {
items: state.items,
addItem,
removeItem,
updateQuantity,
clearCart,
total,
itemCount: state.items.length
};
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 CartProvider');
}
return context;
}
// Usage in component
function CartSummary() {
const { items, total, itemCount, removeItem } = useCart();
return (
<div>
<h2>Shopping Cart ({itemCount} 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: ${total.toFixed(2)}</h3>
</div>
);
}
Exercise 1: Dark Mode Context
Create a complete dark mode system with context that persists theme preference to localStorage.
// Requirements:
// 1. Create ThemeContext with light/dark modes
// 2. Save theme preference to localStorage
// 3. Apply theme class to body element
// 4. Create toggle button component
// 5. Provide custom useTheme hook
Exercise 2: Multi-Language App
Build a language context that supports English, Arabic, and French with translation function.
// Requirements:
// 1. Create LanguageContext with translations object
// 2. Provide t() function for translations
// 3. Change document direction for RTL languages
// 4. Language selector component
// 5. At least 10 translated keys
Exercise 3: Notification System
Create a notification context that manages success/error/info messages with auto-dismiss.
// Requirements:
// 1. NotificationContext with array of notifications
// 2. Methods: addNotification, removeNotification, clearAll
// 3. Auto-dismiss after 3 seconds
// 4. Support types: success, error, info, warning
// 5. NotificationList component to display all
Summary
- Context provides a way to share data across component tree without prop drilling
- Create context with
createContext(), provide with Provider, consume withuseContext - Custom provider patterns encapsulate context logic and state management
- Custom hooks like
useAuth()provide cleaner API and error checking - Split contexts by concern to optimize performance
- Common use cases: theming, authentication, language, shopping cart
- Context updates cause all consumers to re-render - optimize when needed