useReducer is a React hook that keeps state like useState, but moves every way of changing it into one function, the reducer. Your component calls dispatch with an action object such as { type: 'increment' }, and React passes the current state and that action to the reducer, which returns the next state.
The buttons do not say how the count changes, only what happened. All the arithmetic lives in reducer. Add a case 'double' that returns { count: state.count * 2 } and a button that dispatches it to see how a new kind of update fits in.
The syntax
const [state, dispatch] = useReducer(reducer, initialArg, init); // init is optional
function reducer(state, action) {
// return the next state
}
reduceris a function(state, action) => nextState. Define it outside the component; it does not need anything from the component's scope.initialArgis the starting state, used on the first render only.initis optional. If you pass it, the starting state isinit(initialArg)instead.stateis the current state for this render.dispatch(action)sends an action to the reducer and schedules a render with the result. It is stable: it is the same function on every render, so it is safe to pass down or list as a dependency.
An action can be any value, but by convention it is an object with a type string describing what happened, plus whatever data the reducer needs: { type: 'added', text: 'Buy milk' }.
Writing a reducer
Most reducers are a switch on action.type, with one case per kind of event. Each case returns a brand new state; it never changes the old one. The default case throws, so a typo such as dispatch({ type: 'incremnet' }) fails loudly instead of doing nothing.
Name actions after what the user did (added, toggled, deleted), not after the state change you have in mind (setTodos). The component then reads like a list of events, and the reducer is the one place that decides what each event means.
Reducers must be pure
A reducer is a pure function: given the same state and action, it returns the same result, and it does nothing else. That means no mutation, no requests, no timers, no Math.random() or Date.now() inside it. React relies on this. In development under StrictMode, React calls your reducer twice for each action and keeps one result, to help you notice a reducer that is not pure. The preview here runs like a production build, so the examples on this page call it once.
The mutation rule is the one people break most. Pushing into state.todos and returning state returns the same object, so React sees no change and skips the render, the same trap described in updating arrays and objects. Build new arrays and objects with spread, map and filter.
// Wrong: mutates and returns the same object
case 'added':
state.todos.push(action.todo);
return state;
// Right: returns a new object with a new array
case 'added':
return { ...state, todos: [...state.todos, action.todo] };
Side effects that belong to an event, like saving to a server, go in the event handler next to dispatch. Side effects that belong to the state, like syncing it to localStorage, go in an effect.
A todo list with useReducer
Here is the reducer doing real work: adding, toggling and deleting todos. The console.log at the top of the reducer prints every action, which is a handy way to watch how state changes while you debug. It is the one side effect commonly tolerated in a reducer, because it changes nothing; take it out before shipping.
Add a todo, tick a box and delete one, then read the console: every change is one line naming the action and its data. The input's text stays in useState, because it is local to the field and no other event touches it. Mixing the two hooks in one component is normal.
The new todo's id is created in the click handler and travels in the action, so the reducer only copies it. Writing id: nextId++ inside the reducer would make it impure: under StrictMode in development the second call would skip an id.
Dispatch does not change state right away
dispatch works like a useState setter: it schedules a render, and the state variable in your current handler keeps the old value. To use the new state right away, compute it yourself by calling the reducer.
Click a few times. The first log line is always one step behind, and the second matches the number on the button after the render. Because the reducer is a plain function, calling it yourself is safe, which is another benefit of keeping it pure.
useState vs useReducer
Both hooks store state, and anything you write with one you can write with the other. The difference is where the update logic lives.
| useState | useReducer | |
|---|---|---|
| Update logic | In each event handler | In one reducer function |
| Good for | A few independent values | Several fields changed together by many events |
| Code size | Less for simple state | More up front, less as events grow |
| Testing | Test through the component | Test the reducer as a plain function |
| Debugging | Find which handler set it | Log each action in one place |
Reach for useReducer when you notice the same state being updated in many handlers, when one event has to change several pieces of state that must stay consistent, or when a handler is mostly logic about the next state. For a toggle or a text field, useState is shorter and clearer. Switching later is not hard: replace the setters with dispatches and move the logic into cases.
Lazy initialization
If you write the initial state as a function call, such as useReducer(reducer, createInitialState('Ada')), that call runs on every render, even though React only uses its result the first time. If building the initial state is expensive, or should be derived from a prop, pass a third argument: an init function. React calls init(initialArg) once, on the first render.
Type in the input: the component renders on every keystroke, but createInitialState logs only once. Now change the call to useReducer(reducer, createInitialState('Ada')) and type again. The function runs on every render, and React throws the result away each time after the first.
Pass the function itself, not the result of calling it. useReducer(reducer, 'Ada', createInitialState) is lazy; useReducer(reducer, createInitialState('Ada')) is not.
useReducer with context
A reducer pairs well with context when a deep tree needs the state. Put the state and dispatch in context at the top, and any component below can read the state or send actions without props passed through every layer. Because dispatch never changes, components that only send actions can read a separate dispatch context and skip re-rendering when the state changes.
import { createContext, useContext, useReducer } from 'react';
const TodosContext = createContext(null);
const TodosDispatchContext = createContext(null);
export function TodosProvider({ children }) {
const [todos, dispatch] = useReducer(todosReducer, []);
return (
<TodosContext value={todos}>
<TodosDispatchContext value={dispatch}>{children}</TodosDispatchContext>
</TodosContext>
);
}
function AddTodo() {
const dispatch = useContext(TodosDispatchContext);
return <button onClick={() => dispatch({ type: 'added', text: 'New' })}>Add</button>;
}
In React 19 a context object works as its own provider (<TodosContext value={...}>); <TodosContext.Provider> still works too. The useContext page covers how context reaches components and when it re-renders them.
Frequently Asked Questions
What is useReducer in React?
A hook for state whose updates are described as actions. You call const [state, dispatch] = useReducer(reducer, initialState), then dispatch({ type: 'added' }) from event handlers. React passes the current state and the action to your reducer, and whatever it returns becomes the next state.
When should I use useReducer instead of useState?
When several events update the same state in related ways, when the next state depends on several fields at once, or when the update logic is long enough that you want it in one testable function outside the component. For one or two independent values, useState is simpler.
Why must a reducer be pure?
React may call your reducer more than once for the same action (StrictMode does it on purpose in development) and expects the same result each time. So a reducer must not mutate the state, fetch data, set timers or read random values. Do those things in event handlers or effects.
Does dispatch update the state immediately?
No. Like a useState setter, dispatch schedules a render. Inside the current event handler state still holds the old value; the new state appears in the next render.
What is the third argument of useReducer?
An optional init function. When you pass it, React computes the initial state as init(initialArg), and only on the first render. It is useful when building the initial state is expensive or depends on a prop.