Start.dev
Back to blog

React useState vs useReducer: when to use which

useState or useReducer? See the same cart built both ways, the signs it's time to switch, and a simple rule of thumb for choosing in your React components.

Ariel Weinberger
Ariel Weinberger ยท 5 min read

If you've used React for a while, you've probably wondered whether you should be using useState or useReducer. They both store state in a component, and they both trigger a re-render when it changes. So what's the difference?

Short answer: start with useState, and switch to useReducer when your state updates become hard to follow. useState is the right default for simple, independent values. useReducer earns its place when several pieces of state change together, or when the next state depends on a set of clear rules.

They are not rivals. useState is built on the same idea as useReducer, just with less ceremony. If you're still getting comfortable with state, start with React Fundamentals (opens in a new tab) and come back when your components start to feel crowded.

The quick comparison

useStateuseReducer
What you getA value and a setterA state value and a dispatch function
How you updateCall the setter with a new valueDispatch an action describing what happened
Where update logic livesSpread across event handlersIn one reducer function
Best forIndependent values: a toggle, an input, a counterRelated values and multi-step updates
BoilerplateVery littleMore up front
Testing the logicTest through the componentTest the reducer as a plain function

useState: simple and direct

Here is a counter with useState. There is nothing to set up beyond the hook itself.

import { useState } from "react";
 
function Counter() {
  const [count, setCount] = useState(0);
 
  return (
    <button onClick={() => setCount((c) => c + 1)}>
      Clicked {count} times
    </button>
  );
}

For a single value like this, useState is hard to beat. Each piece of state gets its own variable and its own setter, and the code reads top to bottom. If you need a refresher on how updates work, see State With Usestate (opens in a new tab) and Updating From Previous State (opens in a new tab).

useReducer: one place for the rules

With useReducer, you don't set state directly. You describe what happened by dispatching an action, and a reducer function decides what the next state is.

import { useReducer } from "react";
 
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;
  }
}
 
function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
 
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
    </div>
  );
}

For a counter, this is more code than you need, and that's fine. The point is the shape. The reducer takes the current state and an action, and returns the next state. A reducer should be a plain function: no fetching, no timers, and no changing the old state in place.

When useState starts to hurt

The real difference shows up when state gets more complicated. Imagine a shopping cart with useState.

function Cart() {
  const [items, setItems] = useState([]);
  const [total, setTotal] = useState(0);
  const [coupon, setCoupon] = useState(null);
 
  function addItem(item) {
    setItems((prev) => [...prev, { ...item, quantity: 1 }]);
    setTotal((prev) => prev + item.price);
  }
 
  function removeItem(id, price, quantity) {
    setItems((prev) => prev.filter((i) => i.id !== id));
    setTotal((prev) => prev - price * quantity);
  }
 
  // ...more handlers, each one updating several values
}

This works, but notice the risk. Every handler has to remember to update items and total together. Forget one setTotal call and the two values drift apart. The rules for how the cart behaves are spread across many functions.

Now the same cart with useReducer.

function cartReducer(state, action) {
  switch (action.type) {
    case "add": {
      const items = [...state.items, { ...action.item, quantity: 1 }];
      return { ...state, items, total: calculateTotal(items, state.coupon) };
    }
    case "remove": {
      const items = state.items.filter((i) => i.id !== action.id);
      return { ...state, items, total: calculateTotal(items, state.coupon) };
    }
    case "applyCoupon": {
      return {
        ...state,
        coupon: action.coupon,
        total: calculateTotal(state.items, action.coupon),
      };
    }
    default:
      return state;
  }
}
 
function Cart() {
  const [state, dispatch] = useReducer(cartReducer, {
    items: [],
    total: 0,
    coupon: null,
  });
 
  return (
    <button onClick={() => dispatch({ type: "add", item: product })}>
      Add to cart
    </button>
  );
}

The component now only says what happened: add, remove, apply a coupon. The reducer holds every rule for how the cart changes, in one place. The total is recalculated each time, so it can't fall out of sync with the items.

Signs it's time to switch to useReducer

You don't need a rule book. These are the common signals:

  • Several values change together. If one event updates three useState values, they probably belong in one object managed by a reducer.
  • The next state depends on rules. Things like "you can't go below zero" or "a coupon only applies once" are easier to keep in one function.
  • You have many handlers touching the same state. A reducer gives them a single place to go through.
  • You want to test the logic on its own. A reducer is a plain function, so you can test it without rendering anything.
  • State moves through steps. Forms with several stages, or loading, success and error states, fit actions well.

A useful check: if you can describe your updates as a short list of events, such as "item added", "item removed", "coupon applied", a reducer is probably a good fit.

When useState is still the better choice

Don't reach for useReducer just because it feels more advanced. Stick with useState when:

  • The state is a single value, like a modal's open flag or an input's text.
  • Values are independent. A search box and a theme toggle don't need to share a reducer.
  • The update is always "replace this value with that one".

Many components are fine with several small useState calls. Splitting state into separate values is often clearer than one large object, and you can always move to useReducer later without changing how the component looks from the outside.

What about sharing state across components?

A common point of confusion: useReducer is not the same thing as global state. Both hooks keep state inside one component. If other components need that state, you still pass it down as props or share it with context.

That said, they combine well. A popular pattern is to put a reducer's state and dispatch into context, so any component below can read the state or dispatch an action. If you want to learn that side, see Component State Vs Global State (opens in a new tab), Lifting State Up (opens in a new tab), and Creating And Consuming Context (opens in a new tab).

Common mistakes with useReducer

  • Changing state directly in the reducer. Always return a new object or array instead of changing the old one. The lessons Updating Objects In State (opens in a new tab) and Updating Arrays In State (opens in a new tab) cover the same habit for useState.
  • Forgetting the default case. Return the current state for unknown actions so a typo doesn't wipe your state.
  • Doing side effects in the reducer. Fetching data or setting timers belongs in an event handler or an effect, not in the reducer.
  • Using it too early. A reducer for one boolean is extra code with no benefit.

Our recommendation

Start with useState. When you notice several values changing together, or update logic spreading across handlers, move that state into a reducer. Learn the useState habits first in React Fundamentals (opens in a new tab), then use what you learn here to decide when a reducer is worth the extra structure.

Level up your coding skills

Jump into interactive courses and start building real-world projects.

Start learning

Related posts

What useEffect really does, when it runs, and how the dependency array and cleanup function work. Simple examples for fetching data, timers and page titles.
Ariel Weinberger
Ariel Weinberger ยท 5 min read
Learn the JavaScript array methods you will use daily: map, filter, find, some, every, reduce and sort, with runnable examples on one shopping cart.
Ariel Weinberger
Ariel Weinberger ยท 5 min read
Learn what Express middleware is, how next() moves a request through the pipeline, and how to write a logger, an API-key gate and an error handler.
Ariel Weinberger
Ariel Weinberger ยท 4 min read