AcademyReact
Choosing Between useReducer and useState
useReducer is useState generalized: a pure reducer plus dispatch instead of a raw setter, and when that trade is worth making.
3 min readUpdated 3 exercises
useReducer is useState, generalized
useState gives you a value and a setter. useReducer gives you a value and a dispatch function, plus a reducer you write yourself that decides, for every possible action, what the next state is:
function counterReducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
case 'reset': return { count: 0 };
default: return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<button onClick={() => dispatch({ type: 'increment' })}>
{state.count}
</button>
);
}Nothing here is unreachable with useState — this is the same counter you could write with a setter. The difference is where the logic lives. With useState, "what happens on increment" is written inline, in the click handler, at the call site. With useReducer, it is written once, in counterReducer, as a plain function that takes a state and an action and returns a state — no JSX, no hooks, nothing React-specific about it at all.
When useState is enough
Reach for useState when each piece of state changes independently and the update is a direct computation — setOpen(!open), setValue(e.target.value), setCount(c => c + 1). There is no branching logic to centralize; writing a reducer around a single boolean toggle just moves one line into a switch statement.
When useReducer earns its place
Two signals, either one enough on its own:
- The next state depends on more than the action itself — several related values need to change together in one atomic step (a form's
values,errorsandtouchedall shifting from a single "field changed" event), or the same event needs different outcomes depending on the current state (atoggleaction that behaves differently whilestatusis'loading'). - The set of things that can happen is worth naming — a reducer's
switchis a complete, readable list of every transition the state can go through:'fetchStart','fetchSuccess','fetchError','reset'. ScatteredsetStatecalls across several handlers give you no single place that answers "what can happen to this state?"
function formReducer(state, action) {
switch (action.type) {
case 'change':
return { ...state, values: { ...state.values, [action.field]: action.value } };
case 'blur':
return { ...state, touched: { ...state.touched, [action.field]: true } };
case 'reset':
return initialFormState;
default:
return state;
}
}Three fields' worth of useState calls, each needing its own change handler, collapse into one dispatch({ type: 'change', field, value }) call site reused for every input.
The part that makes useReducer worth learning: testability
A reducer is a pure function — same state and action in, same new state out, no rendering, no hooks, no component involved:
expect(counterReducer({ count: 5 }, { type: 'reset' })).toEqual({ count: 0 });You can write that test without React, without a DOM, without rendering anything. State logic exercised through useState setters can only be tested by rendering the component and simulating interaction — a reducer's logic can be tested in isolation, which is the single strongest practical argument for reaching for it once state transitions get complicated enough to be worth testing on their own.
What interviewers are checking
Not whether you can write a reducer — whether you can articulate the trade. useState is simpler and should stay the default; useReducer is the right upgrade exactly when the transitions themselves, not just the values, are the part worth naming and testing.
Now practice it
2 challenges · 1 quizReading this page is the cheap half. These are the exercises that make you use it.
- useReducer HookProImplement useReducer itself — dispatch has to route every action through your own reducer function.ChallengeSolve · MediumChallengeSolve · Medium
- Custom useStateProBuild useState from scratch, which makes the gap between a raw setter and a reducer concrete.ChallengeSolve · MediumChallengeSolve · Medium
- Hooks · Advanced Hook PatternsHarder hook questions that assume you know when a reducer is the right tool, not just how to write one.QuizTake · HardQuizTake · Hard
Read next
How useState Batches and Updates
Why the value you read right after calling a setter is still stale, when the updater-function form is required, and how React batches several setState calls into one render.
3 min
Lifting State Up and Prop Drilling
Why two components that need to stay in sync must share state owned by their closest common ancestor, and when passing that state down several layers becomes a real problem.
3 min