ReactEasy
useState vs useReducer in React — when to use which
Both manage component state. useState is for simple independent values. useReducer is for complex state with multiple related fields or actions.
useState — simple, additive
function Counter() {
const [count, setCount] = useState(0);
return (
<>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</>
);
}
Great when state is one value (or a few independent ones). The setter takes a new value or a function (prev) => next.
When useState starts hurting
function CheckoutForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [address, setAddress] = useState('');
const [city, setCity] = useState('');
const [pincode, setPincode] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [errors, setErrors] = useState({});
// ...8 more setters
}
Seven useStates, no relationship between them in code, every handler has to call multiple setters. This is when reducer wins.
useReducer — actions describe intent
type State = {
step: 'cart' | 'address' | 'review' | 'done';
items: Item[];
address: Address | null;
error: string | null;
};
type Action =
| { type: 'add_item'; item: Item }
| { type: 'remove_item'; id: string }
| { type: 'set_address'; address: Address }
| { type: 'next_step' }
| { type: 'fail'; error: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'add_item': return { ...state, items: [...state.items, action.item] };
case 'remove_item': return { ...state, items: state.items.filter((i) => i.id !== action.id) };
case 'set_address': return { ...state, address: action.address };
case 'next_step': return { ...state, step: nextStep(state.step) };
case 'fail': return { ...state, error: action.error };
}
}
function Checkout() {
const [state, dispatch] = useReducer(reducer, initialState);
// dispatch({ type: 'add_item', item }) anywhere
}
Benefits:
- All state transitions live in one function — easy to follow, test, and TypeScript-check.
- Dispatch a single object instead of calling multiple setters → atomic update.
- The reducer is testable in isolation:
expect(reducer(state, action)).toEqual(...). - Action type union catches typos at compile time.
Decision rule
- 1-3 independent values → useState
- State fields are related, or transitions are complex → useReducer
- State logic appears in multiple components → lift to Context + useReducer, or a state library (Zustand, Redux Toolkit)
Hybrid pattern
You can use both in the same component:
function ProductPage() {
const [isFavorited, setFavorited] = useState(false); // simple
const [cart, dispatchCart] = useReducer(cartReducer, ...); // complex
}
Common useReducer mistake
// ❌ Mutating instead of returning new state
case 'add_item':
state.items.push(action.item); // bug — same reference, no re-render
return state;
// ✅ Always return a new object
case 'add_item':
return { ...state, items: [...state.items, action.item] };
React compares state with Object.is. Same reference = no re-render. Always treat state as immutable.