useReducer
Manages component state through a pure reducer function that maps the current state and an action to the next state.
const [state, dispatch] = useReducer(reducer, initialArg, init?)- Added in React 16.8.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$reducer | (state: S, action: A) => S | The function that specifies how state gets updated. It must be pure, takes the current state and an action as arguments, and returns the next state. State and action can be of any type. |
$initialArg | any | The value from which the initial state is calculated. How it is used depends on whether you also pass init: without init it becomes the initial state directly, with init it is passed to init. |
$initoptional | (initialArg: any) => S | An initializer function returning the initial state. If omitted, the initial state is initialArg; otherwise it is the result of calling init(initialArg). |
Return value
[state: S, dispatch: (action: A) => void]An array with exactly two values: the current state — on the first render, init(initialArg) or initialArg when no init is given — and the dispatch function, which sends an action to the reducer and triggers a re-render. dispatch returns nothing and has a stable identity.
Errors
Nothing by itself, but an error thrown inside your reducer — for example a default case that rejects an unknown action type — propagates out of dispatch to the nearest error boundary.
Changelog
- PHP 18.0.0Dispatches are batched automatically outside React event handlers as well, so several dispatches in a promise or timeout cause one re-render.
- PHP 18.0.0React no longer eagerly runs the reducer for a bail-out check in every case; do not rely on the reducer being called a fixed number of times.
- PHP 18.0.0Strict Mode calls the reducer and the initializer twice in development to surface impurities; one result is discarded.
useReducer is useState with the update logic pulled out of the event handlers and into one pure function. Handlers stop describing how state changes and only announce what happened: dispatch({ type: 'added_item', id }).
When to reach for it
- Several state values must change together and must never drift apart —
status,dataanderror, for instance. - The same transition is triggered from many places, and you want it written once.
- You want to test transitions without rendering: a reducer is a plain function you can call in a unit test.
- A deep child needs to trigger updates; passing one stable
dispatchthrough context beats threading five callbacks.
What trips people up
The reducer must be pure. No fetch, no Date.now(), no mutation of the state argument — in Strict Mode React calls it twice in development, and an impure reducer will visibly double-apply. Return a new object; returning the mutated original means Object.is sees no change and React skips the render.
A reducer that falls through and returns undefined for an unrecognised action wipes your state silently. Throw on unknown action types instead so typos surface immediately.
Example
When to use it
- Coordinate a multi-field form where validation errors, dirty flags and values must stay consistent.
- Model a fetch lifecycle as idle → loading → success → error transitions in one place.
- Drive an undo/redo stack where each action appends to a history array.
- Pass a single stable dispatch down through context instead of a dozen callback props.
Frequently asked questions
When should I use useReducer instead of useState?
Reach for useReducer when several state values must change together, when the same update is triggered from many handlers, or when the update logic is complex enough that you would like to unit-test it on its own. For a single independent value — a boolean, a text field, a counter — useState is shorter and just as correct.
Why is my reducer running twice?
Strict Mode deliberately calls your reducer and initializer twice in development to expose impurities; production runs them once. If the doubled call changes what you see, the reducer is doing something it should not — mutating the state argument, appending to an outside array, calling Date.now(), or firing a request. Move that work into an event handler or effect.
How do I do async work with useReducer?
Do the async work outside the reducer and dispatch when it settles: dispatch a "started" action, await the request in a handler or effect, then dispatch "succeeded" or "failed" with the result. Reducers must be pure and synchronous, so awaiting inside one is never correct.
More examples
A fetch lifecycle as a state machine
The component renders "Loading…", then either "Signed in as …" or "Failed: …" — never a spinner sitting next to a stale name, because every transition rewrites all three fields at once. With three separate useState calls you can reach status: "error" while data still holds the previous response.
import { useReducer, useEffect } from "react";
const initialState = { status: "idle", data: null, error: null };
function reducer(state, action) {
switch (action.type) {
case "started":
// Clearing data and error here is the whole point: the three
// fields can never contradict each other.
return { status: "loading", data: null, error: null };
case "succeeded":
return { status: "success", data: action.data, error: null };
case "failed":
return { status: "error", data: null, error: action.error };
default:
throw new Error("Unknown action: " + action.type);
}
}
export default function UserCard() {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
let cancelled = false;
dispatch({ type: "started" });
fetch("/api/user/1")
.then((res) => res.json())
.then((data) => {
if (!cancelled) dispatch({ type: "succeeded", data });
})
.catch((error) => {
if (!cancelled) dispatch({ type: "failed", error: error.message });
});
return () => {
cancelled = true;
};
}, []);
if (state.status === "loading") return <p>Loading…</p>;
if (state.status === "error") return <p>Failed: {state.error}</p>;
if (state.status === "success") return <p>Signed in as {state.data.name}</p>;
return <p>Idle</p>;
}Sharing dispatch through context
TaskRow updates shared state without receiving a single callback prop. Because dispatch has a stable identity across renders, the DispatchContext value never changes, so components that only read dispatch are not re-rendered when the task list changes.
import { createContext, useContext, useReducer } from "react";
const TasksContext = createContext(null);
const DispatchContext = createContext(null);
const initialTasks = [
{ id: 1, text: "Draft the RFC", done: false },
{ id: 2, text: "Book the room", done: true },
];
function tasksReducer(tasks, action) {
switch (action.type) {
case "added":
return [...tasks, { id: action.id, text: action.text, done: false }];
case "toggled":
return tasks.map((t) =>
t.id === action.id ? { ...t, done: !t.done } : t
);
case "deleted":
return tasks.filter((t) => t.id !== action.id);
default:
throw new Error("Unknown action: " + action.type);
}
}
function TaskRow({ task }) {
// No callback props: the row reaches dispatch directly.
const dispatch = useContext(DispatchContext);
return (
<li>
<input
type="checkbox"
checked={task.done}
onChange={() => dispatch({ type: "toggled", id: task.id })}
/>{" "}
{task.text}{" "}
<button onClick={() => dispatch({ type: "deleted", id: task.id })}>
Delete
</button>
</li>
);
}
function TaskList() {
const tasks = useContext(TasksContext);
return (
<ul>
{tasks.map((task) => (
<TaskRow key={task.id} task={task} />
))}
</ul>
);
}
export default function Board() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={tasks}>
<DispatchContext.Provider value={dispatch}>
<h3>{tasks.filter((t) => !t.done).length} open</h3>
<TaskList />
</DispatchContext.Provider>
</TasksContext.Provider>
);
}
Discussion