useReducer — managing complex state
2 min read
When useState gets complicated
useState is perfect for simple values. But when state gets complex — many related fields, or updates that depend on an action type — juggling several useState calls gets messy. useReducer organizes all that logic in one place.
If you have used array reduce in JavaScript, the idea is similar: take the current state plus an action, and return the new state.
The shape
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>{state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>−</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}
How does it work?
- The reducer is a function:
(currentState, action) => newState. It holds all your update rules in one place. useReducer(reducer, initialState)gives you the currentstateand adispatchfunction.- To update, you dispatch an action — an object like
{ type: "increment" }. React runs the reducer with it and re-renders with the new state.
useState vs useReducer — which one?
- Simple, independent values (a name, a toggle) →
useState. - Complex state with many actions, or next state depends on the action →
useReducer.
It is the same idea as state, just organized: all the "how state changes" logic lives in one tidy reducer function instead of scattered across handlers.
useReducercentralizes complex state logic. Youdispatchan action; areducer(state, action)returns the new state. Reach for it whenuseStatestarts feeling tangled.