useState vs useReducer: How to Choose and When to Switch
useState is fine until it is not. The signal to switch to useReducer is not the number of state values — it is when the next state depends on multiple previous values, or when the same state can change for more than one reason.
useState is the right starting point for almost all state. It is simple, readable, and requires no boilerplate. For a form input, a toggle, a counter, or a loading flag — useState is the correct tool.
The question is not "should I use useReducer?" The question is: has useState become a problem?
The Signals That useState Is Struggling
Signal 1: Multiple state values that change together.
If you find yourself calling two or three setters in the same event handler to keep state in sync, that is a sign the values are not actually independent:
// These three always change together — they are one unit of state
setIsLoading(true);
setData(null);
setError(null);A reducer models this as one transition:
dispatch({ type: 'fetch_started' });
// reducer:
case 'fetch_started':
return { isLoading: true, data: null, error: null };Signal 2: The next state depends on multiple previous values.
The functional update form (prev => prev + 1) handles simple self-referential updates. But when the next value of one state depends on the current value of another, you are working against useState:
// Awkward with useState — you need the latest of both
setTotal(cartItems.reduce((sum, item) => sum + item.price, 0));
setItemCount(cartItems.length);A reducer receives the full current state as state, so cross-field dependencies are natural.
Signal 3: The same state changes for many different reasons.
If a piece of state can change because of user input, a timer, an API response, and a keyboard shortcut, you end up with four separate setter calls scattered across the component. It becomes impossible to see all the ways state can change in one place.
A reducer collects all transitions — regardless of trigger — into a single function. Every possible change to the state is visible in one switch.
Side-by-Side Comparison
// useState — fine for independent, simple values
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// useReducer — better for related, co-changing values
const [state, dispatch] = useReducer(reducer, {
count: 0,
name: '',
isSubmitting: false,
});The choice is not about the number of state variables. A component with ten independent useState calls is cleaner than one useReducer managing ten unrelated things. The criterion is relatedness and coupling, not count.
The Practical Decision
Start with useState. Switch to useReducer when:
- State values change together and should be treated as one unit
- Transitions have business names (
fetch_started,form_submitted,reset) that make the code read like a description of behavior - You want the transition logic to be testable in isolation without mounting a component
- You are building toward a state machine where only certain transitions are valid from certain states
useReducer is not a replacement for useState — it is what you reach for when useState has started to show its limits.
Practice what you just read.
Keep reading