Skip
Arish's avatar

17. Context API for State


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!