useReducer
Move complex state logic into a reducer with actions, and combine it with context.
As components grow, state updates can get scattered across many event handlers: add an item here, remove it there, toggle, edit, reset, undo… useReducer lets you consolidate all of that logic in one function, outside your component, where it's easy to read and test. It's the same pattern Redux popularised, built into React.
From useState to useReducer#
Here's a task list with several handlers, each updating state slightly differently:
With a reducer, handlers just describe what happened by dispatching an action, and one reducer function decides how the state changes:
How it works#
reducer(state, action)— a pure function you write that returns the next state.initialState— the starting value (optionally computed lazily with a thirdinitargument).state— the current state for this render.dispatch(action)— call it to request an update. React runs your reducer with the current state and the action, then re-renders with the result.
An action is a plain object describing what happened. By convention it has a type string, plus any data the reducer needs. Name actions after what happened ("added", "toggled") rather than how to change state ("setTasks").
Reducers must be pure#
A reducer runs during rendering, so it must be pure:
- No mutation. Return new objects and arrays (
...spread,map,filter), neverstate.push(...). - No side effects. No
fetch, timers,localStoragewrites or random values. Generate ids or timestamps in the handler and put them in the action (ascrypto.randomUUID()above). - Same input → same output. In Strict Mode, React calls reducers twice in development to help you spot impurities.
Because reducers are just functions, they're easy to unit test without rendering anything:
Reducers for object state#
Reducers aren't only for lists. Here's a checkout form with several interdependent fields:
All the rules — clamping quantity, validating coupons, step boundaries — live in one place.
Lazy initialisation#
Pass an init function as the third argument to compute the initial state once:
Reducer + context: app-wide state without a library#
Combine a reducer with context to let any component read state and dispatch actions — a lightweight alternative to Redux:
dispatch is guaranteed to be stable (the same function every render), which is why it's safe to put in its own context and pass anywhere.
useState or useReducer?#
You can mix both in one component, and converting between them is easy. Don't force a reducer onto simple state.
Common mistakes#
- Mutating state in the reducer (
state.items.push(x); return state;) — React may not re-render. - Putting side effects (fetching, random ids) inside the reducer.
- Forgetting to return state for unhandled actions — throwing on unknown types catches typos early.
- Reading
stateright afterdispatchand expecting the new value (it's a snapshot, just likeuseState).
What's next#
You've written several reusable chunks of logic already. Next, learn to package them as custom hooks.
Check your understanding
Quick quiz
1.What does a reducer function receive and return?
2.Which is allowed inside a reducer?
3.When is
useReducera better choice thanuseState?
Finished reading?
Mark this lesson complete to track your progress.