useContext - Global State Management
What is Context?
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.
The Problem: Prop Drilling
// ❌ Prop drilling - passing props through many levels
function App() {
const [user, setUser] = useState<User | null>(null);
return <Dashboard user={user} setUser={setUser} />;
}
function Dashboard({ user, setUser }) {
return <Sidebar user={user} setUser={setUser} />;
}
function Sidebar({ user, setUser }) {
return <UserMenu user={user} setUser={setUser} />;
}
function UserMenu({ user, setUser }) {
return <UserProfile user={user} setUser={setUser} />;
}
function UserProfile({ user, setUser }) {
return <div>{user?.name}</div>;
}
Creating Context
Basic Context
import { createContext, useContext, useState } from 'react';
// 1. Create context with default value
const UserContext = createContext<User | null>(null);
// 2. Create provider component
function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
}
// 3. Use context in components
function UserProfile() {
const user = useContext(UserContext);
if (!user) return <div>Not logged in</div>;
return <div>Welcome, {user.name}!</div>;
}
// 4. Wrap app with provider
function App() {
return (
<UserProvider>
<Dashboard />
</UserProvider>
);
}
Context with TypeScript
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
interface UserContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
updateUser: (updates: Partial<User>) => void;
}
// Create context with undefined default (safer)
const UserContext = createContext<UserContextType | undefined>(undefined);
function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (email: string, password: string) => {
const user = await authAPI.login(email, password);
setUser(user);
};
const logout = () => {
setUser(null);
authAPI.logout();
};
const updateUser = (updates: Partial<User>) => {
if (user) {
setUser({ ...user, ...updates });
}
};
const value: UserContextType = {
user,
login,
logout,
updateUser,
};
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
// Custom hook for type safety
function useUser() {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('useUser must be used within UserProvider');
}
return context;
}
// Usage
function UserProfile() {
const { user, logout } = useUser();
return (
<div>
<h1>{user?.name}</h1>
<button onClick={logout}>Logout</button>
</div>
);
}
Multiple Contexts
// Theme Context
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => {
setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Settings Context
interface Settings {
language: string;
notifications: boolean;
timezone: string;
}
const SettingsContext = createContext<
| {
settings: Settings;
updateSettings: (updates: Partial<Settings>) => void;
}
| undefined
>(undefined);
function SettingsProvider({ children }: { children: React.ReactNode }) {
const [settings, setSettings] = useState<Settings>({
language: 'en',
notifications: true,
timezone: 'UTC',
});
const updateSettings = (updates: Partial<Settings>) => {
setSettings(prev => ({ ...prev, ...updates }));
};
return (
<SettingsContext.Provider value={{ settings, updateSettings }}>
{children}
</SettingsContext.Provider>
);
}
// Combine providers
function App() {
return (
<UserProvider>
<ThemeProvider>
<SettingsProvider>
<Dashboard />
</SettingsProvider>
</ThemeProvider>
</UserProvider>
);
}
Provider Composition Pattern
// Create a combined provider
function AppProviders({ children }: { children: React.ReactNode }) {
return (
<UserProvider>
<ThemeProvider>
<SettingsProvider>
<NotificationProvider>{children}</NotificationProvider>
</SettingsProvider>
</ThemeProvider>
</UserProvider>
);
}
function App() {
return (
<AppProviders>
<Dashboard />
</AppProviders>
);
}
Advanced Patterns
Context with Reducer
type State = {
user: User | null;
loading: boolean;
error: string | null;
};
type Action =
| { type: 'LOGIN_START' }
| { type: 'LOGIN_SUCCESS'; payload: User }
| { type: 'LOGIN_ERROR'; payload: string }
| { type: 'LOGOUT' };
const initialState: State = {
user: null,
loading: false,
error: null,
};
function authReducer(state: State, action: Action): State {
switch (action.type) {
case 'LOGIN_START':
return { ...state, loading: true, error: null };
case 'LOGIN_SUCCESS':
return { user: action.payload, loading: false, error: null };
case 'LOGIN_ERROR':
return { ...state, loading: false, error: action.payload };
case 'LOGOUT':
return initialState;
default:
return state;
}
}
const AuthContext = createContext<
| {
state: State;
dispatch: React.Dispatch<Action>;
}
| undefined
>(undefined);
function AuthProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(authReducer, initialState);
return (
<AuthContext.Provider value={{ state, dispatch }}>
{children}
</AuthContext.Provider>
);
}
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
const { state, dispatch } = context;
const login = async (email: string, password: string) => {
dispatch({ type: 'LOGIN_START' });
try {
const user = await authAPI.login(email, password);
dispatch({ type: 'LOGIN_SUCCESS', payload: user });
} catch (error) {
dispatch({ type: 'LOGIN_ERROR', payload: error.message });
}
};
const logout = () => {
dispatch({ type: 'LOGOUT' });
};
return {
user: state.user,
loading: state.loading,
error: state.error,
login,
logout,
};
}
Context with Persistence
function createPersistedContext\<T\>(key: string, defaultValue: T) {
const Context = createContext<
| {
value: T;
setValue: (value: T) => void;
}
| undefined
>(undefined);
function Provider({ children }: { children: React.ReactNode }) {
const [value, setValue] = useState\<T\>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : defaultValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [value]);
return (
<Context.Provider value={{ value, setValue }}>
{children}
</Context.Provider>
);
}
function useValue() {
const context = useContext(Context);
if (!context) {
throw new Error('useValue must be used within Provider');
}
return context;
}
return { Provider, useValue };
}
// Usage
const { Provider: ThemeProvider, useValue: useTheme } = createPersistedContext(
'theme',
'light'
);
function App() {
return (
<ThemeProvider>
<Dashboard />
</ThemeProvider>
);
}
function ThemeToggle() {
const { value: theme, setValue: setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
);
}
Selective Context Updates
// Split context to prevent unnecessary re-renders
const UserStateContext = createContext<User | null>(null);
const UserActionsContext = createContext<
| {
updateUser: (updates: Partial<User>) => void;
logout: () => void;
}
| undefined
>(undefined);
function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const actions = useMemo(
() => ({
updateUser: (updates: Partial<User>) => {
setUser(prev => (prev ? { ...prev, ...updates } : null));
},
logout: () => {
setUser(null);
},
}),
[]
);
return (
<UserStateContext.Provider value={user}>
<UserActionsContext.Provider value={actions}>
{children}
</UserActionsContext.Provider>
</UserStateContext.Provider>
);
}
// Hooks to access specific parts
function useUserState() {
const context = useContext(UserStateContext);
if (context === undefined) {
throw new Error('useUserState must be used within UserProvider');
}
return context;
}
function useUserActions() {
const context = useContext(UserActionsContext);
if (context === undefined) {
throw new Error('useUserActions must be used within UserProvider');
}
return context;
}
// Component only re-renders when actions are called, not when user state changes
function LogoutButton() {
const { logout } = useUserActions();
return <button onClick={logout}>Logout</button>;
}
// Component re-renders when user changes
function UserDisplay() {
const user = useUserState();
return <div>{user?.name}</div>;
}
Real-World Examples
Shopping Cart Context
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartContextType {
items: CartItem[];
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
total: number;
}
const CartContext = createContext<CartContextType | undefined>(undefined);
function CartProvider({ children }: { children: React.ReactNode }) {
const [items, setItems] = useState<CartItem[]>([]);
const addItem = (item: Omit<CartItem, 'quantity'>) => {
setItems(prev => {
const existing = prev.find(i => i.id === item.id);
if (existing) {
return prev.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
);
}
return [...prev, { ...item, quantity: 1 }];
});
};
const removeItem = (id: string) => {
setItems(prev => prev.filter(item => item.id !== id));
};
const updateQuantity = (id: string, quantity: number) => {
if (quantity <= 0) {
removeItem(id);
return;
}
setItems(prev =>
prev.map(item => (item.id === id ? { ...item, quantity } : item))
);
};
const clearCart = () => {
setItems([]);
};
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return (
<CartContext.Provider
value={{ items, addItem, removeItem, updateQuantity, clearCart, total }}
>
{children}
</CartContext.Provider>
);
}
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within CartProvider');
}
return context;
}
Performance Optimization
Memoize Context Value
function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
// ❌ New object on every render
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
// ✅ Memoized value
const value = useMemo(() => ({ user, setUser }), [user]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
Split Contexts for Performance
// Instead of one large context
// Split into smaller, focused contexts
<UserProvider>
<ThemeProvider>
<NotificationProvider>{children}</NotificationProvider>
</ThemeProvider>
</UserProvider>
Best Practices
- Create custom hooks - Always wrap useContext in a custom hook
- Validate context - Throw error if used outside provider
- Memoize values - Prevent unnecessary re-renders
- Split contexts - Keep contexts focused and small
- Use TypeScript - Type safety for context values
- Avoid overuse - Don't use context for every piece of state
- Consider alternatives - Props, composition, state management libraries
When NOT to Use Context
- ❌ Frequently changing data (use state management library)
- ❌ Local component state (use useState)
- ❌ Props that are only passed down 1-2 levels
- ❌ Performance-critical applications with many updates
Practice Exercises
- Create a theme context with light/dark mode
- Build an authentication context with login/logout
- Implement a notification/toast context
- Create a multi-language i18n context
- Build a shopping cart context
Next Steps
- Learn about useReducer
- Explore Custom Hooks