Menu Close

useState vs useReducer: Choosing the Right State Hook

useState and useReducer both do the same fundamental job: they let a component hold state that persists between renders and trigger a re-render when that state changes. But they represent two different philosophies for how state should be updated, and picking the wrong one for a given component can leave you either over-engineering a simple toggle or fighting a tangle of related state variables that drift out of sync with each other.

useState vs useReducer update flow diagram

useState: Direct and Simple

useState gives you a piece of state and a setter function. You call the setter with a new value, and React re-renders with that value in place.

const [count, setCount] = useState(0);

function increment() {
  setCount(count + 1);
}

This is the right tool when a piece of state is independent, and updates to it don’t need to know about other pieces of state. A boolean toggle, a text input’s value, a selected tab index — these are all naturally useState territory. The update logic is simple enough to write inline, right where the event happens.

useReducer: Centralized and Predictable

useReducer separates the what happened from the how state changes. Instead of calling a setter directly, you dispatch an action — a plain object describing an event — and a reducer function decides how that action transforms the current state into the next state.

function reducer(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;
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0 });

dispatch({ type: 'increment' });

Notice what changed: the component no longer contains the update logic itself. It just describes what happened (‘increment’) and lets the reducer decide what that means for the state. This separation is the whole point of useReducer.

Why This Separation Matters

The value of useReducer becomes clear once your state has more than one moving part that need to change together, consistently, no matter where in the component the update is triggered from.

Consider a form with several fields, validation state, and a submission status. With useState, you’d likely end up with five or six separate state variables, and any event that needs to touch more than one of them (submitting the form should set isSubmitting to true and clear errors and increment submitCount) has to remember to update all of them together, every time, in every handler that does this. Miss one in a new handler six months from now, and you have a subtle bug.

With useReducer, that logic lives in exactly one place — the reducer — and every part of the component that wants to trigger this compound update just dispatches a single action:

function reducer(state, action) {
  switch (action.type) {
    case 'submit_start':
      return { ...state, isSubmitting: true, errors: {}, submitCount: state.submitCount + 1 };
    case 'submit_success':
      return { ...state, isSubmitting: false };
    case 'submit_error':
      return { ...state, isSubmitting: false, errors: action.errors };
    default:
      return state;
  }
}

No matter how many places in the component dispatch 'submit_start', the resulting state transition is always identical, because it’s defined once. That consistency is difficult to guarantee with several independent useState calls scattered through the same component.

The Trade-off: Boilerplate vs Guarantees

useReducer isn’t free. It requires you to define action types, write a reducer function, and dispatch objects instead of just calling a setter. For a single counter or a toggle, that’s meaningfully more code to express the same behavior:

// useState: 1 line
const [isOpen, setIsOpen] = useState(false);

// useReducer: several lines for the same result
function reducer(state, action) {
  switch (action.type) {
    case 'toggle': return !state;
    default: return state;
  }
}
const [isOpen, dispatch] = useReducer(reducer, false);

For simple, independent state, this extra structure buys you nothing — it’s pure overhead. That’s why reaching for useReducer by default, even for trivial state, tends to make components harder to read rather than easier.

A Simple Decision Rule

Ask: does an update to this state ever need to depend on, or affect, other state at the same time?

  • If each piece of state changes independently, with no coordination needed — use useState. Most component state fits here.
  • If several values need to update together consistently, or the next state depends on complex logic involving the previous state — use useReducer. Forms, wizards, multi-step flows, and any state machine-like behavior are the classic cases.

A useful signal: if you find yourself calling multiple setX functions back-to-back inside a single event handler, especially if you do that in more than one handler, that’s usually a sign the state belongs in a reducer instead.

They’re Not Mutually Exclusive

It’s common, and often correct, to use both in the same component. A component might use useReducer for a complex piece of coordinated state (like a multi-step form) while using useState for something unrelated and simple (like whether a tooltip is currently visible). Neither hook is “better” globally — they’re suited to different shapes of state within the same component.

Quick Reference

useState useReducer
Update style Call a setter directly Dispatch an action, reducer computes next state
Best for Independent, simple values Coordinated state, complex transitions
Boilerplate Minimal More upfront (action types, reducer function)
Predictability Update logic can end up scattered across handlers Update logic centralized in one function
Classic use case Toggles, inputs, simple counters Forms, wizards, state machines

The Takeaway

useState and useReducer aren’t a beginner-vs-advanced pair — they’re suited to different shapes of state. useState keeps simple, independent values simple. useReducer keeps complex, coordinated state consistent by centralizing the update logic in one place instead of scattering it across handlers. The question isn’t which hook is more powerful — it’s whether your state’s next value depends only on itself, or on a rule that needs to be applied the same way everywhere it’s triggered.

Leave a Reply

Your email address will not be published. Required fields are marked *