React useReducer Hook
Manage complex state logic with the useReducer hook, actions, reducers, and common patterns.
TL;DR
- 01Use useReducer for state with several related update rules.
- 02Write a reducer function that handles each action type.
- 03Dispatch actions to trigger state changes in components.
Tips
- 01Pair <code>useReducer</code> with <code>useContext</code> to build a clean global store without pulling in Redux or Zustand for small apps.
Warnings
- 01Never mutate the state object directly inside a reducer, since React relies on a new reference to detect changes and re-render.
Basic Usage
- Import
useReducerfrom React at the top of your component file.import { useReducer } from "react"; - Call
useReducerwith a reducer function and an initial state value.const [state, dispatch] = useReducer(reducer, { count: 0 }); - The hook returns the current state and a dispatch function for actions.
- Reducers are pure functions that take state and an action, then return new state.
- Use this pattern when state updates depend on the current state.
Writing Reducers
- Define a reducer function that handles each action type with a switch.
function reducer(state, action) { switch (action.type) { case "increment": return { count: state.count + 1 }; case "decrement": return { count: state.count - 1 }; default: return state; } } - Always return a new state object instead of mutating the existing one.
- Include a default case to handle unknown action types safely.
- Keep reducers pure — no API calls, timers, or random values inside.
- This makes state changes predictable and easy to test.
Dispatching Actions
- Call
dispatchwith an action object to trigger a state update.<button onClick={() => dispatch({ type: "increment" })}> + </button> - Actions are plain objects with a required
typefield. - Add extra fields to pass data along with the action.
dispatch({ type: "add", payload: 5 }); - The reducer reads these fields to compute the new state.
- Dispatch can be passed down to child components through props.
When to Use It
- Reach for
useReducerwhen state has many related update paths. - Use it when the next state depends on multiple pieces of current state.
- Use it when actions need to be predictable and easy to test.
- Use
useStatefor simple booleans, strings, and counters instead. - Use Context plus reducer for app-wide state without external libraries.
Common Patterns
- Initialize lazily with a third argument for expensive setup logic.
const [state, dispatch] = useReducer(reducer, props, (p) => ({ count: p.initial })); - Type actions with a discriminated union in TypeScript for safety.
type Action = | { type: "increment" } | { type: "add"; payload: number }; - Pair
useReducerwithuseContextto share state across the tree. - Split large reducers into smaller helper functions for clarity.
- Use
useImmerReducerfrom libraries when deep updates feel tedious.
FAQ
Reach for useReducer when your state has multiple sub-values that change together, or when the next state depends on the previous one in complex ways. If you find yourself writing several related useState calls or passing many setters down as props, useReducer is likely the cleaner choice.
It takes a reducer function and an initial state value: const [state, dispatch] = useReducer(reducer, initialState). An optional third argument lets you pass an init function to lazily compute the initial state, which is useful for expensive setup.
Use a switch statement on action.type, returning a new state object for each case and a default that returns the current state unchanged. Each case should spread the existing state and override only the fields that change: return { ...state, count: state.count + 1 }.
For small-to-medium apps it often can — combine useReducer with useContext to share state across the tree without a third-party library. Redux still has advantages for large apps that need DevTools time-travel debugging, middleware, or a single store across many independent feature modules.
React compares state by reference, so if you return the same object (or array) the component won't re-render. Always return a new object from every reducer branch — use the spread operator or Object.assign instead of modifying the existing state in place.