React · Lesson 8 of 15
useReducer and Complex State
Model state as events and a reducer when useState starts to sprawl.
- Intermediate
- 16 min read
- 3 objectives
Before this lessonLesson 7: Data Fetching and Routing
What you will learn
- Write a reducer
- Dispatch actions
- Choose useReducer vs useState
Your Progress
0 of 15 lessons 0%
- Lessons0 / 15
- Completed0
- Est. time left~ 4 hours
Create a free account to keep your progress on every device.
When a component has several related pieces of state that update together, a pile of useState calls becomes hard to follow. useReducer puts the next-state logic in one function: you dispatch an event, the reducer returns the new state.
A reducer is a pure function
It takes the current state and an action, and returns the next state. It must not mutate the previous object.
function cartReducer(state, action) {
switch (action.type) {
case "add":
return { ...state, items: [...state.items, action.item] };
case "remove":
return { ...state, items: state.items.filter((i) => i.id !== action.id) };
case "clear":
return { ...state, items: [] };
default:
throw new Error(`Unknown action ${action.type}`);
}
}Wiring it up
import { useReducer } from "react";
const initial = { items: [] };
function Cart() {
const [state, dispatch] = useReducer(cartReducer, initial);
const total = state.items.reduce((sum, i) => sum + i.price, 0);
return (
<>
<button onClick={() => dispatch({ type: "add", item: { id: 1, price: 9 } })}>
Add
</button>
<button onClick={() => dispatch({ type: "clear" })}>Clear</button>
<p>{state.items.length} items · ${total}</p>
</>
);
}dispatch is stable across renders, so you can pass it down without wrapping it in useCallback.
When to prefer it
- Several fields change together (a form wizard, a cart, a game board).
- The next state depends on the previous one in a non-trivial way.
- You want the update rules in one testable function, outside the component.
// Write your solution here
