useReducer
The Kanban board and shopping cart projects both had state that could change in several distinct ways — add, remove, update quantity, move between columns — each handled by its own separate function calling setItems or setColumns with slightly different logic. As the number of "ways state can change" grows, that logic ends up scattered across many handlers. useReducer centralizes all of it into one place: a single function describing every possible state transition.
A Reducer Is Just a Function
A reducer takes the current state and an action describing what happened, and returns the new state — nothing more. It's a plain function, with no React-specific code inside it at all; every possible transition lives in one readable place instead of being spread across separate handleIncrement/handleDecrement/handleReset functions.
useReducer in a Component
useReducer(reducer, initialState) returns [state, dispatch] — state is read exactly like useState's value, but instead of a setter, dispatch sends an action object to the reducer, which decides what the new state should be. No button handler needs to know how to update the count — each one just describes what happened ("increment"), leaving the actual logic entirely inside the reducer.
Actions with a Payload
payload is just a convention — a place to attach whatever extra information a particular action needs (which todo's id to toggle or remove, the text for a new one). Project 1's todo list scattered this same add/toggle/remove logic across three separate state-updating functions; written as a reducer, all three transitions live together, each one still using the same immutable-update rules (spread, .map(), .filter()) from Fundamentals Chapter 3 and Project 1 — just consolidated into a single function instead of three.
Pairing useReducer with Context
useReducer and Context combine naturally for global state: the reducer's state becomes the context's value, and dispatch itself can be exposed through context too, letting any deeply nested component dispatch an action without ever needing the state-updating logic passed down to it as props. This combination is genuinely how some real apps implement global state without reaching for an external library at all — a more powerful version of the CartProvider pattern from Project 6, with one centralized reducer instead of several separate updater functions.
state argument directly, call an API, set a timer, or do anything else with a side effect — given the same state and action, it must always return the same result, with no exceptions. It's also easy to forget the default case in the switch — without one, dispatching an unrecognized action type returns undefined instead of the unchanged state, silently wiping out everything.
| Situation | Reach for... |
|---|---|
| One or two simple, independent values | useState |
| Several related values that change together, in many distinct ways | useReducer |
| The same reducer state needed deep in the tree, widely | useReducer + Context |
Coding Challenges
Build a Counter component using useReducer with "increment", "decrement", and "reset" action types, matching the chapter's example exactly.
📄 View solutionRefactor a todo list (add, toggle complete, remove) to use a single useReducer instead of separate useState-driven handlers, using "add"/"toggle"/"remove" action types with a payload carrying the relevant id/text.
📄 View solutionBuild a NotificationContext using useReducer + Context together, supporting "show" (adds a message) and "dismiss" (removes one by id) actions, consumed by two unrelated components — one that triggers a notification, one that displays the current list of them.
📄 View solutionChapter 3 Quick Reference
- A reducer is a pure function:
(state, action) => newState - useReducer(reducer, initialState) returns
[state, dispatch] - dispatch({ type, payload }) — describes what happened, not how to update state
- Centralizes many related state transitions in one place, instead of scattering them across handlers
- Pairs naturally with Context for global state, without an external library
- A reducer must stay pure — no mutation, no side effects, and always include a
defaultcase - Next chapter: custom hooks — extracting reusable logic of your own, the way useTheme did in Chapter 2