Back to articles
ReactHooksState

useState vs useReducer: A Simple Rule That Ends the Debate

Use useState for independent values; reach for useReducer when multiple pieces of state change together or the next state depends on the last. Here's the reasoning, with real examples.

Yash Thakur
4 min read
useState vs useReducer: A Simple Rule That Ends the Debate

The short answer: use useState when your state is a handful of independent values, and switch to useReducer when several pieces of state change together, when the next state depends on the previous one, or when the update logic has grown into a tangle of setThis and setThat across many handlers. That one distinction resolves almost every real case. The rest of this is why.

Why the question even comes up

Both hooks do the same fundamental thing: hold a value across renders and trigger a re-render when it changes. You can technically build any component with either. So the choice isn't about capability — it's about which one makes your update logic easier to read and harder to get wrong. I've shipped components that started with three tidy useState calls and slowly rotted into a dozen interdependent ones, and the rewrite to useReducer is always the same relief.

useState is right more often than Twitter suggests

Most state is genuinely independent: a toggle, an input value, a selected tab. For those, useState is the correct, boring answer. Don't let the "reducers are more scalable" discourse push you into ceremony you don't need.

Three unrelated values, three setters, nothing coordinates them. A reducer here would be pure overhead.

The tell: state that changes together

The moment updating one value forces you to also update another to keep them consistent, you have related state, and that relationship wants to live in one place. The classic example is any async request: loading, error, and data are not independent — they move as a unit.

Every handler has to remember to update all three correctly. Miss one and you get a spinner that never stops or an error shown next to stale data. A reducer makes each transition a single named intent that sets the whole consistent state at once:

Now there's no way to be "loading and errored" at once, because each action returns a complete, valid state. (If that discriminated-union shape looks useful, it's the same idea I lean on in Writing Clean, Maintainable React.)

The other tell: next state depends on last state

Counters, undo/redo, steppers, anything accumulating — if the update is "take what's there and transform it," a reducer centralizes that logic instead of scattering setX(x => ...) callbacks. It also makes the transitions testable: a reducer is a pure function, so you can assert reducer(before, action) === after with no React at all.

A practical migration path

You don't have to decide up front. Start with useState — it's cheaper to write and read. When you notice the smell (three-plus related values, handlers that update several setters together, "impossible" states appearing in bug reports), that's the signal to consolidate into a reducer. Refactoring useState → useReducer is mechanical and low-risk, so there's no penalty for starting simple.

The rule, one more time

Independent values → useState. State that moves together, or depends on its previous value, or whose update logic has outgrown the component → useReducer. You're not picking the "scalable" hook or the "simple" hook; you're picking the one that matches the shape of your state. Get that match right and the debate stops being interesting — which is exactly where you want it.

Written by Yash Thakur

Senior React Developer · 8+ years building for the web

More articles

Keep reading