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.

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
useState | useReducer | |
|---|---|---|
| What you get | A value and a setter | A state value and a dispatch function |
| How you update | Call the setter with a new value | Dispatch an action describing what happened |
| Where update logic lives | Spread across event handlers | In one reducer function |
| Best for | Independent values: a toggle, an input, a counter | Related values and multi-step updates |
| Boilerplate | Very little | More up front |
| Testing the logic | Test through the component | Test 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
useStatevalues, 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.









