Context API for State Management
The Context API provides a way to pass data through the component tree without prop drilling. Combined with hooks, it's a powerful state management solution.
Basic Context State Pattern
jsx
1import { createContext, useContext, useState } from 'react'
2
3// 1. Create context
4const CounterContext = createContext(null)
5
6// 2. Create provider component
7function CounterProvider({ children }) {
8 const [count, setCount] = useState(0)
9
10 const increment = () => setCount(c => c + 1)
11 const decrement = () => setCount(c => c - 1)
12 const reset = () => setCount(0)
13
14 const value = { count, increment, decrement, reset }
15
16 return (
17 <CounterContext.Provider value={value}>
18 {children}
19 </CounterContext.Provider>
20 )
21}
22
23// 3. Create custom hook
24function useCounter() {
25 const context = useContext(CounterContext)
26 if (!context) {
27 throw new Error('useCounter must be used within CounterProvider')
28 }
29 return context
30}
31
32// 4. Use in components
33function CounterDisplay() {
34 const { count } = useCounter()
35 return <h1>Count: {count}</h1>
36}
37
38function CounterButtons() {
39 const { increment, decrement, reset } = useCounter()
40 return (
41 <div>
42 <button onClick={decrement}>-</button>
43 <button onClick={increment}>+</button>
44 <button onClick={reset}>Reset</button>
45 </div>
46 )
47}
48
49// 5. Wrap app with provider
50function App() {
51 return (
52 <CounterProvider>
53 <CounterDisplay />
54 <CounterButtons />
55 </CounterProvider>
56 )
57}Shopping Cart Context
Real-world example:
jsx
1import { createContext, useContext, useReducer } from 'react'
2
3// Actions
4const ACTIONS = {
5 ADD_ITEM: 'ADD_ITEM',
6 REMOVE_ITEM: 'REMOVE_ITEM',
7 UPDATE_QUANTITY: 'UPDATE_QUANTITY',
8 CLEAR_CART: 'CLEAR_CART'
9}
10
11// Reducer
12function cartReducer(state, action) {
13 switch (action.type) {
14 case ACTIONS.ADD_ITEM: {
15 const existingItem = state.items.find(
16 item => item.id === action.payload.id
17 )
18
19 if (existingItem) {
20 return {
21 ...state,
22 items: state.items.map(item =>
23 item.id === action.payload.id
24 ? { ...item, quantity: item.quantity + 1 }
25 : item
26 )
27 }
28 }
29
30 return {
31 ...state,
32 items: [...state.items, { ...action.payload, quantity: 1 }]
33 }
34 }
35
36 case ACTIONS.REMOVE_ITEM:
37 return {
38 ...state,
39 items: state.items.filter(item => item.id !== action.payload)
40 }
41
42 case ACTIONS.UPDATE_QUANTITY:
43 return {
44 ...state,
45 items: state.items.map(item =>
46 item.id === action.payload.id
47 ? { ...item, quantity: action.payload.quantity }
48 : item
49 )
50 }
51
52 case ACTIONS.CLEAR_CART:
53 return { ...state, items: [] }
54
55 default:
56 return state
57 }
58}
59
60// Context
61const CartContext = createContext(null)
62
63// Provider
64function CartProvider({ children }) {
65 const [state, dispatch] = useReducer(cartReducer, { items: [] })
66
67 const addItem = (product) => {
68 dispatch({ type: ACTIONS.ADD_ITEM, payload: product })
69 }
70
71 const removeItem = (productId) => {
72 dispatch({ type: ACTIONS.REMOVE_ITEM, payload: productId })
73 }
74
75 const updateQuantity = (productId, quantity) => {
76 dispatch({ type: ACTIONS.UPDATE_QUANTITY, payload: { id: productId, quantity } })
77 }
78
79 const clearCart = () => {
80 dispatch({ type: ACTIONS.CLEAR_CART })
81 }
82
83 const totalItems = state.items.reduce((sum, item) => sum + item.quantity, 0)
84 const totalPrice = state.items.reduce(
85 (sum, item) => sum + item.price * item.quantity, 0
86 )
87
88 const value = {
89 items: state.items,
90 totalItems,
91 totalPrice,
92 addItem,
93 removeItem,
94 updateQuantity,
95 clearCart
96 }
97
98 return (
99 <CartContext.Provider value={value}>
100 {children}
101 </CartContext.Provider>
102 )
103}
104
105// Hook
106function useCart() {
107 const context = useContext(CartContext)
108 if (!context) {
109 throw new Error('useCart must be used within CartProvider')
110 }
111 return context
112}
113
114// Usage
115function ProductCard({ product }) {
116 const { addItem } = useCart()
117
118 return (
119 <div className="product-card">
120 <h3>{product.name}</h3>
121 <p>${product.price}</p>
122 <button onClick={() => addItem(product)}>Add to Cart</button>
123 </div>
124 )
125}
126
127function CartIcon() {
128 const { totalItems } = useCart()
129
130 return (
131 <div className="cart-icon">
132 🛒 <span className="badge">{totalItems}</span>
133 </div>
134 )
135}
136
137function CartSummary() {
138 const { items, totalPrice, removeItem, updateQuantity, clearCart } = useCart()
139
140 return (
141 <div className="cart-summary">
142 <h2>Your Cart</h2>
143 {items.map(item => (
144 <div key={item.id} className="cart-item">
145 <span>{item.name}</span>
146 <input
147 type="number"
148 value={item.quantity}
149 onChange={(e) => updateQuantity(item.id, parseInt(e.target.value))}
150 min="1"
151 />
152 <span>${item.price * item.quantity}</span>
153 <button onClick={() => removeItem(item.id)}>Remove</button>
154 </div>
155 ))}
156 <div className="total">Total: ${totalPrice.toFixed(2)}</div>
157 <button onClick={clearCart}>Clear Cart</button>
158 </div>
159 )
160}Theme Context
jsx
1const ThemeContext = createContext(null)
2
3function ThemeProvider({ children }) {
4 const [theme, setTheme] = useState('light')
5
6 const toggleTheme = () => {
7 setTheme(prev => prev === 'light' ? 'dark' : 'light')
8 }
9
10 // Apply theme to document
11 useEffect(() => {
12 document.documentElement.setAttribute('data-theme', theme)
13 }, [theme])
14
15 return (
16 <ThemeContext.Provider value={{ theme, toggleTheme }}>
17 {children}
18 </ThemeContext.Provider>
19 )
20}
21
22function useTheme() {
23 const context = useContext(ThemeContext)
24 if (!context) {
25 throw new Error('useTheme must be used within ThemeProvider')
26 }
27 return context
28}Multiple Contexts
jsx
1function App() {
2 return (
3 <AuthProvider>
4 <ThemeProvider>
5 <CartProvider>
6 <NotificationProvider>
7 <Router>
8 <Main />
9 </Router>
10 </NotificationProvider>
11 </CartProvider>
12 </ThemeProvider>
13 </AuthProvider>
14 )
15}
16
17// Clean up with a combined provider
18function AppProviders({ children }) {
19 return (
20 <AuthProvider>
21 <ThemeProvider>
22 <CartProvider>
23 <NotificationProvider>
24 {children}
25 </NotificationProvider>
26 </CartProvider>
27 </ThemeProvider>
28 </AuthProvider>
29 )
30}
31
32function App() {
33 return (
34 <AppProviders>
35 <Router>
36 <Main />
37 </Router>
38 </AppProviders>
39 )
40}Performance Optimization
Split context to prevent unnecessary re-renders:
jsx
1// ❌ Bad: All consumers re-render when any value changes
2const AppContext = createContext()
3
4function AppProvider({ children }) {
5 const [user, setUser] = useState(null)
6 const [theme, setTheme] = useState('light')
7
8 return (
9 <AppContext.Provider value={{ user, setUser, theme, setTheme }}>
10 {children}
11 </AppContext.Provider>
12 )
13}
14
15// ✅ Good: Separate contexts for unrelated state
16const UserContext = createContext()
17const ThemeContext = createContext()
18
19// ✅ Even better: Split state and dispatch
20const CartStateContext = createContext()
21const CartDispatchContext = createContext()
22
23function CartProvider({ children }) {
24 const [state, dispatch] = useReducer(cartReducer, initialState)
25
26 return (
27 <CartStateContext.Provider value={state}>
28 <CartDispatchContext.Provider value={dispatch}>
29 {children}
30 </CartDispatchContext.Provider>
31 </CartStateContext.Provider>
32 )
33}
34
35// Components that only dispatch won't re-render on state changes
36function AddButton({ product }) {
37 const dispatch = useContext(CartDispatchContext)
38 return <button onClick={() => dispatch({ type: 'ADD', payload: product })}>Add</button>
39}Context API is perfect for medium-sized apps without external dependencies!
