React useReducer Hook
Manage complex state transitions and multi-step actions predictably using pure reducer functions with useReducer.
TL;DR
- Consolidate complex state transitions using the pure
useReducerhook. - Dispatch descriptive
actionobjects specifying transition type and payload. - Enforce compile-time safety across state actions using TypeScript
discriminatedunions.
Reducer Fundamentals
Pure Reducer FunctionCalculate next state from current state and action object.
function counterReducer(
state: State,
action: Action
): State {
switch (action.type) {
case 'inc': return { count: state.count + 1 };
case 'dec': return { count: state.count - 1 };
default: return state;
}
}Hook InvocationInitialize hook with reducer function and initial state.
export function Counter() {
const [state, dispatch] = useReducer(
counterReducer,
{ count: 0 }
);
const inc = () => dispatch({ type: 'inc' });
return <Button onClick={inc}>{state.count}</Button>;
}Action with PayloadPass supplementary data alongside the action type key.
dispatch({ type: 'setValue', payload: 42 });TypeScript Action Typing
Discriminated UnionEnforce exact payload contracts per action type variant.
type Action =
| { type: 'reset' }
| { type: 'setFilter'; payload: string }
| { type: 'addItem'; payload: Item };Exhaustive Check GuardEnsure all action types are handled using TypeScript never.
default: {
const _exhaustive: never = action;
return state;
}Lazy InitializationSupply third initializer argument for costly computations.
const [state, dispatch] = useReducer(
reducer,
initialArg,
(init) => ({ items: loadFromDisk(init) })
);Complex State Workflows
Form State ReducerManage multi-field form changes and validation states.
function formReducer(
state: FormState,
action: FormAction
) {
switch (action.type) {
case 'change':
return { ...state, [action.field]: action.val };
case 'reset':
return initialFormState;
}
}Async Request State MachineModel loading, error, and data states explicitly.
type State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User }
| { status: 'error'; error: Error };Undo and Redo ArchitectureMaintain past, present, and future state history arrays.
interface HistoryState {
past: State[];
present: State;
future: State[];
}Reducer Best Practices
Keep Reducers PureReducers must never mutate state or make API requests.
// BAD: Side effect in reducer
case 'save':
fetch('/api'); return state;Immutable State UpdatesAlways return new objects with spread operator syntax.
case 'update':
return { ...state, count: action.payload };Dispatch PassingPass dispatch down via context without causing re-renders.
<DispatchContext.Provider value={dispatch}>Tips
- Always keep reducer functions pure by computing next state without mutating arguments or triggering
side-effectsduring calculation. - Model state actions as descriptive event intentions like
todoAddedrather than imperative setters likesetTodo.
Warnings
- Never perform asynchronous operations or network fetches inside reducer functions because
reducersmust remain strictly synchronous. - Avoid using
useReducerfor simple independent primitive values whereuseStateoffers cleaner code ergonomics.
In Practice
Implement an immutable task manager supporting additions, toggles, and deletions.
- Define state structure and discriminated union action types.
- Implement pure reducer function handling add, toggle, and remove.
- Initialize useReducer hook inside the parent component.
- Dispatch strongly-typed actions from UI buttons.
type Task = { id: string; done: boolean };
type Action =
| { type: 'add' }
| { type: 'toggle'; id: string };
function reducer(state: Task[], a: Action): Task[] {
switch (a.type) {
case 'add':
return [...state, { id: '1', done: false }];
case 'toggle':
return state.map((t) =>
t.id === a.id ? { ...t, done: !t.done } : t
);
}
}
export function TaskApp() {
const [tasks, dispatch] = useReducer(reducer, []);
const add = () => dispatch({ type: 'add' });
return <Button onClick={add}>{tasks.length}</Button>;
}FAQ
Choose useReducer when component state involves multiple sub-values, when the next state depends intimately on previous state, or when state transitions follow complex business logic.
No. Reducers are strictly synchronous pure functions. To execute async logic, perform the network call inside an event handler or effect, then dispatch action results upon completion.
Define a discriminated union type where every action interface shares a common type property with distinct string literal values, enabling TypeScript to narrow payloads automatically.