Context & Global State
Checking access...
As your app grows, you’ll find yourself passing props through many levels of components — a problem called prop drilling. React Context provides a way to share values between components without explicitly passing props through every level.
The Problem: Prop Drilling
function App() { const [user, setUser] = useState(null); return <Layout user={user} setUser={setUser} />;}
function Layout({ user, setUser }) { return ( <div> <Header user={user} setUser={setUser} /> <Sidebar user={user} /> <Content user={user} /> </div> );}
function Header({ user, setUser }) { return ( <nav> <UserMenu user={user} onLogout={() => setUser(null)} /> </nav> );}The user prop passes through Layout — which doesn’t need it — just to reach Header and UserMenu.
createContext and useContext
import { createContext, useContext } from "react";
// 1. Create the contextconst ThemeContext = createContext(null);
// 2. Provide the valuefunction App() { const [theme, setTheme] = useState("light");
return ( <ThemeContext.Provider value={{ theme, setTheme }}> <Layout /> </ThemeContext.Provider> );}
// 3. Consume the value anywhere in the treefunction Navbar() { const { theme, setTheme } = useContext(ThemeContext);
return ( <div className={`navbar navbar-${theme}`}> <span>Current theme: {theme}</span> <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}> Toggle Theme </button> </div> );}Any component inside <ThemeContext.Provider> can access the context value — no props needed.
Creating a Context Module
Organise contexts in their own files:
import { 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 for existing session on mount checkSession() .then(setUser) .finally(() => setLoading(false)); }, []);
const login = async (email, password) => { const user = await authAPI.login(email, password); setUser(user); };
const logout = async () => { await authAPI.logout(); setUser(null); };
return ( <AuthContext.Provider value={{ user, loading, login, logout }}> {children} </AuthContext.Provider> );}
export function useAuth() { const context = useContext(AuthContext); if (!context) { throw new Error("useAuth must be used within an AuthProvider"); } return context;}Usage anywhere in the app:
import { useAuth } from "./contexts/AuthContext";
function Profile() { const { user, logout } = useAuth();
if (!user) return <p>Not logged in</p>;
return ( <div> <h2>{user.name}</h2> <button onClick={logout}>Log Out</button> </div> );}Context + useReducer
For complex state, combine Context with useReducer instead of useState:
import { createContext, useContext, useReducer } from "react";
const CartContext = createContext(null);
const initialState = { items: [], total: 0,};
function cartReducer(state, action) { switch (action.type) { case "ADD_ITEM": { const existing = state.items.find((i) => i.id === action.item.id); const items = existing ? state.items.map((i) => i.id === action.item.id ? { ...i, quantity: i.quantity + 1 } : i ) : [...state.items, { ...action.item, quantity: 1 }];
return { ...state, items, total: calculateTotal(items) }; }
case "REMOVE_ITEM": const items = state.items.filter((i) => i.id !== action.id); return { ...state, items, total: calculateTotal(items) };
case "UPDATE_QUANTITY": const updated = state.items.map((i) => i.id === action.id ? { ...i, quantity: Math.max(0, action.quantity) } : i ).filter((i) => i.quantity > 0); return { ...state, items: updated, total: calculateTotal(updated) };
case "CLEAR_CART": return initialState;
default: return state; }}
function calculateTotal(items) { return items.reduce((sum, item) => sum + item.price * item.quantity, 0);}
export function CartProvider({ children }) { const [cart, dispatch] = useReducer(cartReducer, initialState);
return ( <CartContext.Provider value={{ cart, dispatch }}> {children} </CartContext.Provider> );}
export function useCart() { const context = useContext(CartContext); if (!context) { throw new Error("useCart must be used within a CartProvider"); } return context;}Usage:
import { useCart } from "./contexts/CartContext";
function ProductCard({ product }) { const { dispatch } = useCart();
return ( <div> <h3>{product.name}</h3> <p>${product.price}</p> <button onClick={() => dispatch({ type: "ADD_ITEM", item: product })}> Add to Cart </button> </div> );}
function CartSummary() { const { cart } = useCart();
return ( <div> <h2>Cart ({cart.items.length} items)</h2> <p>Total: ${cart.total.toFixed(2)}</p> {cart.items.map((item) => ( <CartItem key={item.id} item={item} /> ))} </div> );}
function CartItem({ item }) { const { dispatch } = useCart();
return ( <div> <span>{item.name}</span> <button onClick={() => dispatch({ type: "REMOVE_ITEM", id: item.id })}> ✕ </button> </div> );}Multiple Contexts
You can use multiple providers, nested:
function AppProviders({ children }) { return ( <AuthProvider> <ThemeProvider> <CartProvider> {children} </CartProvider> </ThemeProvider> </AuthProvider> );}Each context is independent. A component can consume any or all of them:
function UserDashboard() { const { user } = useAuth(); const { theme } = useTheme(); const { cart } = useCart();
// ...}Context Performance Caveats
When context value changes, all consumers re-render. For frequently changing values, consider:
1. Split contexts by concern
// ❌ One context for everything — any change re-renders all consumers<AppContext.Provider value={{ user, theme, cart, notifications }} />
// ✅ Split into separate contexts<AuthProvider> <ThemeProvider> <CartProvider> <NotificationProvider> <App /> </NotificationProvider> </CartProvider> </ThemeProvider></AuthProvider>2. Memoize context values
function ThemeProvider({ children }) { const [theme, setTheme] = useState("light");
// Without useMemo: new object every render → all consumers re-render const value = useMemo(() => ({ theme, setTheme }), [theme]);
return ( <ThemeContext.Provider value={value}> {children} </ThemeContext.Provider> );}When to Use Context vs Props vs State
| Pattern | When to Use | Example |
|---|---|---|
| Props | Passing data to direct children | <Button label="Click" /> |
| Lifting state | Siblings share state | Parent manages shared state |
| Context | Deep tree sharing | Theme, auth, locale, cart |
| State manager (Redux/Zustand) | Complex global state, many updates | Large app with complex interactions |
Quick Reference
// Create contextconst MyContext = createContext(defaultValue);
// Provide value<MyContext.Provider value={someValue}> <Children /></MyContext.Provider>
// Consume valueconst value = useContext(MyContext);
// Custom hook patternexport function useMyContext() { const ctx = useContext(MyContext); if (!ctx) throw new Error("useMyContext must be used within MyProvider"); return ctx;}
// Context + reducerconst [state, dispatch] = useReducer(reducer, initialState);<Context.Provider value={{ state, dispatch }}> {children}</Context.Provider>Practice Exercises
Theme switcher with Context: Create a
ThemeContextwiththemeandtoggleTheme. Provide it at the app level. Consume it in a Navbar, a Card, and a Button component. All three should respond to theme changes.Authentication context with reducer: Build an
AuthContextthat usesuseReducerwith actions:LOGIN,LOGOUT,SET_LOADING. Create a customuseAuthhook. Use it in a protected route setup.Shopping cart with context + reducer: Build a full cart system with context and reducer. Include actions: ADD_ITEM, REMOVE_ITEM, UPDATE_QUANTITY, CLEAR_CART. Create a cart icon in the header showing item count.