How do you manage state in React using useState and useReducer?
useState fits 1–3 independent values updated with simple setters. useReducer fits state with multiple related fields or complex transitions — pull the update logic into a pure reducer (state, action) → state. Reducers are easier to test, easier to log, and naturally handle multi-step state machines. Reach for useReducer when setState calls start to multiply or several pieces of state must change together.
Same problem from two angles.
useState — small, independent values
const [count, setCount] = useState(0);
const [name, setName] = useState('');
setCount(c => c + 1); // functional updater
setName(e.target.value);Best when:
- State is 1–3 independent values.
- Updates are simple (
setX(newValue)orsetX(prev => ...)). - The fields don't constrain each other.
useReducer — state machines & related fields
type State = { items: Todo[]; filter: 'all' | 'done' | 'todo' };
type Action =
| { type: 'add'; text: string }
| { type: 'toggle'; id: string }
| { type: 'setFilter'; filter: State['filter'] };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'add':
return { ...state, items: [...state.items, { id: uid(), text: action.text, done: false }] };
case 'toggle':
return {
...state,
items: state.items.map(t => t.id === action.id ? { ...t, done: !t.done } : t),
};
case 'setFilter':
return { ...state, filter: action.filter };
}
}
const [state, dispatch] = useReducer(reducer, { items: [], filter: 'all' });
dispatch({ type: 'add', text: 'Buy milk' });When to switch from useState → useReducer
- Multiple setState calls in one handler ('set this, then this, then this').
- State fields that must move together (loading + error + data triplet).
- A clear state machine: idle → loading → success / error.
- You want to log/replay actions for debugging.
- Update logic is non-trivial enough that you'd want to unit-test it.
Hybrid: lazy init + reducer
const [state, dispatch] = useReducer(reducer, initialArg, init);init(initialArg) runs once on mount — handy for hydrating from localStorage.
Reducer + Context for app state
const StateCtx = createContext<State | null>(null);
const DispatchCtx = createContext<React.Dispatch<Action> | null>(null);
function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, initial);
return (
<StateCtx.Provider value={state}>
<DispatchCtx.Provider value={dispatch}>{children}</DispatchCtx.Provider>
</StateCtx.Provider>
);
}Split contexts so consumers of only dispatch don't re-render when state changes.
Pitfalls
- Mutating state inside a reducer (
state.items.push(...)) — React won't detect the change. - Putting non-serializable values (promises, class instances) in state — breaks DevTools/replay.
- Using useReducer for state that's really server state — reach for React Query instead.
Server state ≠ client state
If your reducer mostly handles loading/error/data from a fetch, you're rebuilding React Query. Use that instead, and keep useReducer for UI/wizard/form state.
Follow-up questions
- •How do useReducer and Redux differ?
- •When should reducer + context replace a state library?
- •Why must reducers be pure?
Common mistakes
- •Mutating draft state directly without Immer — React doesn't detect the change.
- •Reducer dispatched from inside the reducer — infinite loop.
- •Modeling server state with useReducer when React Query/SWR fits better.
Performance considerations
- •useReducer doesn't change React's render cost — it changes how you write the updates. For very hot updates, useReducer can win slightly because there's a single state object instead of N useState slots, and React can batch one big update.
Edge cases
- •If the reducer is heavy, dispatching inside an effect can cascade rapidly.
- •Context + reducer re-renders every consumer on every state change — split contexts.
- •StrictMode dev double-invokes reducers; they must be pure.
Real-world examples
- •Multi-step forms, drag-and-drop boards (Trello-like), wizards, complex filters, the React DevTools 'Profiler' tab itself. Redux is essentially useReducer + middleware + global store.