Advanced Context Patterns
Move beyond basics to build scalable, performant global state management systems using Context API. Learn optimization techniques and best practices.
🎯 Context with useReducer
// authContext.js
import { createContext, useContext, useReducer } from 'react';
const AuthContext = createContext();
const authReducer = (state, action) => {
switch (action.type) {
case 'LOGIN':
return {
...state,
user: action.payload,
isAuthenticated: true,
loading: false
};
case 'LOGOUT':
return {
...state,
user: null,
isAuthenticated: false
};
case 'UPDATE_PROFILE':
return {
...state,
user: { ...state.user, ...action.payload }
};
default:
return state;
}
};
export function AuthProvider({ children }) {
const [state, dispatch] = useReducer(authReducer, {
user: null,
isAuthenticated: false,
loading: true
});
const login = async (email, password) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
const user = await response.json();
dispatch({ type: 'LOGIN', payload: user });
};
const logout = () => {
dispatch({ type: 'LOGOUT' });
};
const updateProfile = (updates) => {
dispatch({ type: 'UPDATE_PROFILE', payload: updates });
};
return (
<AuthContext.Provider value={{ ...state, login, logout, updateProfile }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);
⚡ Splitting Contexts for Performance
// ❌ Bad: Single context causes unnecessary re-renders
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [cart, setCart] = useState([]);
return (
<AppContext.Provider value={{ user, theme, cart, setUser, setTheme, setCart }}>
{children}
</AppContext.Provider>
);
}
// ✅ Good: Split into multiple contexts
const UserContext = createContext();
const ThemeContext = createContext();
const CartContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
function CartProvider({ children }) {
const [cart, setCart] = useState([]);
return (
<CartContext.Provider value={{ cart, setCart }}>
{children}
</CartContext.Provider>
);
}
// Usage
function App() {
return (
<UserProvider>
<ThemeProvider>
<CartProvider>
<MyApp />
</CartProvider>
</ThemeProvider>
</UserProvider>
);
}
🎨 Memoizing Context Value
// ❌ Bad: Creates new object on every render
function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
// ✅ Good: Memoize the value
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const value = useMemo(() => ({ user, setUser }), [user]);
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}
// ✅ Even better: Split state and dispatch
const UserStateContext = createContext();
const UserDispatchContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
<UserStateContext.Provider value={user}>
<UserDispatchContext.Provider value={setUser}>
{children}
</UserDispatchContext.Provider>
</UserStateContext.Provider>
);
}
// Components only re-render when they use changed context
function UserProfile() {
const user = useContext(UserStateContext); // Re-renders on user change
return <div>{user?.name}</div>;
}
function LoginButton() {
const setUser = useContext(UserDispatchContext); // Never re-renders
return <button onClick={() => setUser({ name: 'John' })}>Login</button>;
}
🏗️ Context Factory Pattern
// createContext.js - Reusable context factory
import { createContext, useContext, useState } from 'react';
export function createCtx(defaultValue) {
const Context = createContext(defaultValue);
function Provider({ children, value }) {
return <Context.Provider value={value}>{children}</Context.Provider>;
}
function useCtx() {
const context = useContext(Context);
if (context === undefined) {
throw new Error('useCtx must be used within Provider');
}
return context;
}
return [Provider, useCtx];
}
// Usage
const [ThemeProvider, useTheme] = createCtx();
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeProvider value={{ theme, setTheme }}>
<MyApp />
</ThemeProvider>
);
}
function ThemedButton() {
const { theme, setTheme } = useTheme();
return (
<button
style={{ background: theme === 'dark' ? '#333' : '#fff' }}
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
Toggle Theme
</button>
);
}
🔐 Complete Auth Context
// contexts/AuthContext.jsx
import { createContext, useContext, useReducer, useEffect } from 'react';
const AuthContext = createContext();
const initialState = {
user: null,
loading: true,
error: null
};
function authReducer(state, action) {
switch (action.type) {
case 'AUTH_START':
return { ...state, loading: true, error: null };
case 'AUTH_SUCCESS':
return { user: action.payload, loading: false, error: null };
case 'AUTH_ERROR':
return { ...state, loading: false, error: action.payload };
case 'LOGOUT':
return { user: null, loading: false, error: null };
default:
return state;
}
}
export function AuthProvider({ children }) {
const [state, dispatch] = useReducer(authReducer, initialState);
// Check authentication on mount
useEffect(() => {
const checkAuth = async () => {
const token = localStorage.getItem('token');
if (!token) {
dispatch({ type: 'LOGOUT' });
return;
}
try {
const response = await fetch('/api/me', {
headers: { Authorization: `Bearer ${token}` }
});
const user = await response.json();
dispatch({ type: 'AUTH_SUCCESS', payload: user });
} catch (error) {
dispatch({ type: 'AUTH_ERROR', payload: error.message });
}
};
checkAuth();
}, []);
const login = async (email, password) => {
dispatch({ type: 'AUTH_START' });
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (!response.ok) throw new Error('Login failed');
const { user, token } = await response.json();
localStorage.setItem('token', token);
dispatch({ type: 'AUTH_SUCCESS', payload: user });
} catch (error) {
dispatch({ type: 'AUTH_ERROR', payload: error.message });
}
};
const logout = () => {
localStorage.removeItem('token');
dispatch({ type: 'LOGOUT' });
};
const value = { ...state, login, logout };
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
🛒 Shopping Cart Context
// contexts/CartContext.jsx
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 = (product) => {
dispatch({ type: 'ADD_ITEM', payload: product });
};
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 itemCount = state.items.reduce(
(count, item) => count + item.quantity,
0
);
const value = {
items: state.items,
addItem,
removeItem,
updateQuantity,
clearCart,
total,
itemCount
};
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}
export const useCart = () => useContext(CartContext);
🎯 Best Practices
- Split contexts: Separate frequently changing data from static data
- Memoize values: Use useMemo to prevent unnecessary re-renders
- useReducer for complex state: Better than multiple useState
- Custom hooks: Always create useContext wrapper hooks
- Error handling: Check if context exists in custom hooks
- Provider placement: Place as close to consumers as possible
- Type safety: Use TypeScript for better developer experience
- State persistence: Sync with localStorage when needed