0% completed
Combining useContext and useReducer Hooks
On This Page
Step 1: Write the Reducer
Step 2: Provide the State and the Dispatch Function
Step 3: Export Hooks Instead of the Contexts
Step 4: Use the Cart in Components
Step 5: Follow the Clicks
When to Use useReducer with Context
A theme is one string with two possible values, and a single setTheme call is enough to change it. A shopping cart is different: it is a list of items, and the user can add a product, add the same product again, remove one line, or empty the whole cart. Each of those changes needs its own rule, and several distant components need both the current cart and a way to change it.
React Fundamentals introduced useReducer for this kind of state: all the update rules live in one reducer function, and components describe what happened by dispatching an action. Context then delivers the reducer's state and its dispatch function to every component that needs them. This lesson adds a cart to the store page with that combination.
Step 1: Write the Reducer
The reducer receives the current list of items and an action, and returns the next list. It never changes the list it receives; it always returns a new array.
// CartContext.jsx import { createContext, useContext, useReducer } from 'react'; export function cartReducer(items, action) { switch (action.type) { case 'added': { const existing = items.find((item) => item.id === action.product.id); if (existing) { return items.map((item) => item.id === action.product.id ? { ...item, quantity: item.quantity + 1 } : item ); } return [...items, { ...action.product, quantity: 1 }]; } case 'removed': return items.filter((item) => item.id !== action.id); case 'cleared': return []; default: throw new Error('Unknown action: ' + action.type); } }
Because the reducer is a plain function, you can test it without rendering anything. Adding the same keyboard twice produces one line with a quantity of 2:
reducer alone: [{"id":1,"name":"Keyboard","price":40,"quantity":2}]
The default branch throws instead of returning the old state. A misspelled action type such as 'add' then fails immediately with Unknown action: add, instead of being ignored without any message.
Step 2: Provide the State and the Dispatch Function
The provider component calls useReducer and passes its two results down. It uses two contexts, one for the items and one for dispatch:
// CartContext.jsx (continued) const CartContext = createContext(null); const CartDispatchContext = createContext(null); export function CartProvider({ children }) { const [items, dispatch] = useReducer(cartReducer, []); return ( <CartContext value={items}> <CartDispatchContext value={dispatch}> {children} </CartDispatchContext> </CartContext> ); }
The two contexts change at different times. The items array is replaced on every cart update, but React gives dispatch a stable identity: it is the same function on every render. A component that only dispatches actions, such as an "Add to cart" button, can therefore read CartDispatchContext alone and is not re-rendered when the items change. The run in Step 5 shows this with render counts.
Step 3: Export Hooks Instead of the Contexts
Instead of exporting the two context objects, the file exports two small custom hooks. Each one reads its context and fails with a clear message when a component is used outside CartProvider:
// CartContext.jsx (continued) export function useCart() { const items = useContext(CartContext); if (items === null) { throw new Error('useCart must be used inside a CartProvider'); } return items; } export function useCartDispatch() { const dispatch = useContext(CartDispatchContext); if (dispatch === null) { throw new Error('useCartDispatch must be used inside a CartProvider'); } return dispatch; }
The default value null makes that check possible. Without the check, a component outside the provider would receive null and fail later with a less helpful error, such as "Cannot read properties of null".
Exporting hooks also prevents a common import mistake. A component file that writes import { CartContext } from './CartContext' fails to build if CartContext is not exported, and with hooks there is nothing else to import.
Step 4: Use the Cart in Components
// App.jsx import { CartProvider, useCart, useCartDispatch } from './CartContext'; const products = [ { id: 1, name: 'Keyboard', price: 40 }, { id: 2, name: 'Mouse', price: 25 }, ]; function AddButton({ product }) { const dispatch = useCartDispatch(); return ( <button onClick={() => dispatch({ type: 'added', product })}> Add {product.name} </button> ); } function CartBadge() { const items = useCart(); const count = items.reduce((sum, item) => sum + item.quantity, 0); return <span>Cart ({count})</span>; } function CartList() { const items = useCart(); const dispatch = useCartDispatch(); const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0); return ( <div> <ul> {items.map((item) => ( <li key={item.id}> {item.name} x {item.quantity} <button onClick={() => dispatch({ type: 'removed', id: item.id })}>Remove</button> </li> ))} </ul> <p>Total: ${total}</p> <button onClick={() => dispatch({ type: 'cleared' })}>Clear cart</button> </div> ); } export default function App() { return ( <CartProvider> <header><CartBadge /></header> {products.map((product) => ( <AddButton key={product.id} product={product} /> ))} <CartList /> </CartProvider> ); }
CartBadge and CartList sit in different parts of the page, yet both show the same cart, because both read the same provider. The count and the total are computed from items during rendering instead of being stored as separate state, so they can never disagree with the list.
Step 5: Follow the Clicks
We rendered App, clicked through the cart, and printed the screen text after each step. We also added a counter to each component that records how many times it rendered during that step:
initial: Cart (0) | Total: $0 {"CartBadge":1,"AddButton Keyboard":1,"AddButton Mouse":1,"CartList":1} Add Keyboard: Cart (1) | Keyboard x 1 | Total: $40 {"CartBadge":1,"AddButton Keyboard":0,"AddButton Mouse":0,"CartList":1} Add Keyboard again: Cart (2) | Keyboard x 2 | Total: $80 {"CartBadge":1,"AddButton Keyboard":0,"AddButton Mouse":0,"CartList":1} Add Mouse: Cart (3) | Keyboard x 2 | Mouse x 1 | Total: $105 Remove first: Cart (1) | Mouse x 1 | Total: $25 Clear: Cart (0) | Total: $0
Here is what happens on the first "Add Keyboard" click:
AddButtoncallsdispatch({ type: 'added', product }).- React calls
cartReducerwith the empty list and that action, and receives a new array with one keyboard. CartProviderre-renders with the newitems. TheCartContextvalue has changed, because the old and the new arrays are different objects.- React re-renders the components that read
CartContext:CartBadgeandCartList. They show "Cart (1)" and "Total: $40". - The
CartDispatchContextvalue is the samedispatchfunction as before, so the twoAddButtoncomponents are not re-rendered.
The diagram separates the components that re-rendered from the ones React skipped. The skip in step 5 depends on one more detail. CartProvider re-renders, but its children are elements that App created, and App did not re-render. React therefore reuses those child elements and only updates the components that read a changed context.
The remaining steps repeat the same pattern, and every one of them showed 0 renders for both AddButton components.
When to Use useReducer with Context
| Approach | Good for | Example |
|---|---|---|
useState in a provider | One or two values with simple updates | Theme, language |
useReducer in a provider | State with several kinds of change, read in many places | Cart, a multi-step form, a feature's filters |
| A state library such as Redux | Large app-wide state with many updates, middleware and developer tools | Chapter 7 |
The reducer and context pattern needs no extra library, and it keeps every update rule in one function that you can test alone. It also has limits: every component that reads the items context re-renders on every cart change, and context offers no tools for inspecting each action. The last lesson of this chapter covers when those limits matter. For data that comes from a server, such as the product list itself, chapter 5 introduces React Query, which handles caching and refetching that a reducer does not.
Reading Progress
0%
On This Page
Step 1: Write the Reducer
Step 2: Provide the State and the Dispatch Function
Step 3: Export Hooks Instead of the Contexts
Step 4: Use the Cart in Components
Step 5: Follow the Clicks
When to Use useReducer with Context