The useReducer Hook in React manages state by running a pure reducer function you write. Instead of calling setter functions inside event handlers, you dispatch actions that describe what happened, and the reducer returns the next state. This keeps every state transition in one place, which pays off once updates start depending on each other.
A reducer is just a function
A reducer takes the current state and an action, then returns the next state. It has no access to props, refs, or other Hooks, so all of its behavior is visible from its two arguments.
function reducer(state, action) {
if (action.type === "incremented") {
return { count: state.count + 1 };
}
return state;
}When the action type is incremented, the reducer returns a new object with count one higher. Returning the same state unchanged tells React there is nothing new to render.
Connect the reducer with useReducer
useReducer accepts the reducer and an initial value, and returns two things: the current state and a dispatch function. Dispatch is how handlers hand an action to the reducer.
import { useReducer } from "react";
function reducer(count, action) {
if (action.type === "incremented") return count + 1;
return count;
}
export default function Counter() {
const [count, dispatch] = useReducer(reducer, 0);
return <button onClick={() => dispatch({ type: "incremented" })}>Count: {count}</button>;
}Clicking the button dispatches an action with the type incremented. React runs the reducer with the current count and that action, stores the returned value, and re-renders the button with the new number.
Group related updates with a switch
Most reducers use a switch statement so each action type has its own case. A counter with increment, decrement, and reset actions shows how one reducer holds every transition for the same state.
function reducer(count, action) {
switch (action.type) {
case "incremented":
return count + 1;
case "decremented":
return count - 1;
default:
return count;
}
}Each case returns a new value for one user interaction, and every branch is visible in one place instead of spread across several handlers. The default case returns the current state for any action the reducer does not recognize.
Update objects and arrays without mutation
A reducer must replace objects and arrays, never change them in place. For a task list, adding a task returns a new array built with the spread operator.
function tasksReducer(tasks, action) {
switch (action.type) {
case "added":
return [...tasks, { id: action.id, text: action.text }];
case "removed":
return tasks.filter((task) => task.id !== action.id);
default:
return tasks;
}
}The spread and filter calls produce fresh arrays, so React sees a different reference and re-renders the list. Mutating the existing tasks array would make React skip the update because the reference stays the same.
Initialize state lazily
When building the initial state is expensive, pass an initializer function as the third argument. React calls it only during the first render.
function createInitialCount() {
return { count: 0, history: [] };
}
function reducer(state, action) {
if (action.type === "incremented") return { ...state, count: state.count + 1 };
return state;
}
function Counter() {
const [state, dispatch] = useReducer(reducer, null, createInitialCount);
return <button onClick={() => dispatch({ type: "incremented" })}>Count: {state.count}</button>;
}Pass createInitialCount itself, not createInitialCount(), so the work runs once. The null second argument means the initializer needs no extra data.
Mistakes to avoid
- Mutating state in the reducer. Always return a new object or array.
- Dispatching during render. Every dispatch schedules another render, so the component loops forever.
- Forgetting to return state in a case. The next state becomes undefined.
Choose a reducer when updates get complex
A reducer adds boilerplate, so it is not automatically better than useState. When one independent value changes at a time, keep useState. When several fields move together, see how to write actions and reducers for the naming rules, then read the useState vs useReducer comparison to decide which fits.
Rune AI
Key Insights
- useReducer manages state through a pure reducer function.
- Handlers dispatch actions that describe what happened.
- The reducer returns the next state without mutating it.
- Pass an initializer as the third argument to avoid rebuilding initial state.
- Keep useState for simple independent values.
Frequently Asked Questions
What does useReducer return?
When should I use useReducer instead of useState?
Why does my reducer run twice in development?
Conclusion
useReducer moves state update logic out of your component and into one pure reducer. Handlers dispatch actions that describe what happened, and the reducer returns the next state without mutating the old one. Reach for it when state updates start to depend on each other.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.