useReducer — State Changes by Name
The reading list can now add a book, remove one, mark one as read, and clear finished ones. Each handler has its own setBooks(...) with its own copy-and-change logic. Spread across a component, that logic is hard to see as a whole. useReducer gathers it into one place.
The rule
Handlers say what happened; the reducer decides what changes.
import { useReducer } from 'react';
function booksReducer(books, action) {
switch (action.type) {
case 'added':
return [...books, action.book];
case 'removed':
return books.filter(b => b.id !== action.id);
case 'marked_read':
return books.map(b => b.id === action.id ? { ...b, read: true } : b);
default:
throw new Error('Unknown action: ' + action.type);
}
}
function ReadingList() {
const [books, dispatch] = useReducer(booksReducer, initialBooks);
return books.map(book => (
<BookCard
key={book.id}
book={book}
onRemove={() => dispatch({ type: 'removed', id: book.id })}
onRead={() => dispatch({ type: 'marked_read', id: book.id })}
/>
));
}Handlers say what happened. Instead of computing the next array, a handler dispatches an action: a plain object with a type and whatever details are needed. Past-tense names like 'added' read naturally.
The reducer decides what changes. booksReducer(state, action) receives the current state and the action and returns the next state. React calls it and re-renders with the result.
The reducer follows the same rules as rendering: it must be pure. No fetching, no timers, no mutating the old state — always return a new value. Because it is a plain function with no React inside, you can test it by calling it directly: booksReducer([], { type: 'added', book }) should return an array holding that one book.
The default case throws on an unknown action type. That turns a typo such as 'remove' instead of 'removed' into a loud error instead of a silent no-op, which is much easier to track down.
The shortcut
Choose by counting: one or two simple updates → useState; many kinds of update to the same data → useReducer. Both give identical results; the reducer just organises the logic.
Click
The user clicks Remove on Dune (
id: 'b2').
A common slip: forgetting to return in a case, which makes the reducer return undefined and wipes the state.
Drills
Answer fast — trust the rule.
What does useReducer(booksReducer, initialBooks) return?
What does a reducer receive?
Which reducer case is correct for 'added'?
A case forgets its return, so the reducer returns undefined for that action. What happens?
Remember
- Handlers say what happened; the reducer decides what changes.
- Many kinds of update to the same data →
useReducer. - Trap: mutating state, side effects, or a missing
returninside the reducer.
