useReducer
useReducer manages state with a reducer function. It is useful when state transitions are more complex than a few independent useState calls.
const [state, dispatch] = useReducer(reducer, initialState);The reducer receives the current state and an action, then returns the next state.
Basic Example
import { useReducer } from "react";
const initialState = { count: 0 };
function counterReducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return initialState;
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>Add</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</>
);
}Why Reducers Help
Reducers centralize transitions.
Instead of spreading update logic across many handlers, you describe what happened.
dispatch({ type: "fieldChanged", field: "email", value: nextEmail });
dispatch({ type: "submitStarted" });
dispatch({ type: "submitFailed", errors });This makes complex flows easier to test and reason about.
Reducers Must Be Pure
A reducer should not:
- mutate state
- make network requests
- read from the DOM
- generate random values for important state transitions
It should return the next state based on state and action.
useReducer vs useState
Use useState when state is simple and updates are local.
Use useReducer when:
- many updates affect the same state object
- one event changes several fields
- transitions have names
- state logic deserves unit tests
Lazy Initialization
For expensive initial state, pass an initializer.
function init(savedCart) {
return savedCart ?? { items: [] };
}
const [cart, dispatch] = useReducer(cartReducer, savedCart, init);Common Mistakes
Do not dispatch during render. Dispatch in event handlers, effects, or callbacks.
Do not mutate nested arrays or objects inside the reducer.
// Wrong
state.items.push(action.item);
return state;Return a new object.
return { ...state, items: [...state.items, action.item] };What should a reducer return?
Practice Challenge
Build a reducer for a checkout form.
Actions:
fieldChangedsubmitStartedsubmitSucceededsubmitFailedreset
Include state for values, errors, isSubmitting, and status.
Recap
useReducer is best when state transitions are named and connected. Keep reducers pure, return new state objects, and use dispatch to describe events instead of manually coordinating many setters.