React course menu
React useReducer Hook: Managing Complex State
Learn useReducer: moving state update logic into a reducer function, dispatching actions, writing reducers with switch, keeping reducers pure, comparing useReducer and useState, and combining a reducer with context.
On this page
When a component has many event handlers that update the same state in different ways, the update logic gets spread across the component. useReducer gathers it into one function, the reducer, so all the ways the state can change are in one place.
The pieces#
const [state, dispatch] = useReducer(reducer, initialState);reducer(state, action): a pure function that takes the current state and an action (an object describing what happened) and returns the next state.dispatch(action): what event handlers call instead of a setter, for exampledispatch({ type: "added", text: "Milk" }).
Handlers say what happened; the reducer decides how the state changes.
A shopping cart#
import { useReducer } from "react";
const PRODUCTS = [
{ id: "tea", name: "Assam tea 250g", price: 180 },
{ id: "honey", name: "Honey 500g", price: 320 },
{ id: "oats", name: "Oats 1kg", price: 210 },
];
function cartReducer(cart, action) {
switch (action.type) {
case "added": {
const existing = cart.find((line) => line.id === action.product.id);
if (existing) {
return cart.map((line) => (line.id === action.product.id ? { ...line, qty: line.qty + 1 } : line));
}
return [...cart, { ...action.product, qty: 1 }];
}
case "decremented":
return cart
.map((line) => (line.id === action.id ? { ...line, qty: line.qty - 1 } : line))
.filter((line) => line.qty > 0);
case "cleared":
return [];
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
export default function App() {
const [cart, dispatch] = useReducer(cartReducer, []);
const total = cart.reduce((sum, line) => sum + line.price * line.qty, 0);
return (
<div>
{PRODUCTS.map((p) => (
<button key={p.id} onClick={() => dispatch({ type: "added", product: p })} style={{ marginRight: 6 }}>
+ {p.name}
</button>
))}
<ul>
{cart.map((line) => (
<li key={line.id}>
{line.name} × {line.qty} = ₹{line.price * line.qty}{" "}
<button onClick={() => dispatch({ type: "decremented", id: line.id })}>−</button>
</li>
))}
</ul>
<p>
<strong>Total: ₹{total}</strong>{" "}
{cart.length > 0 && <button onClick={() => dispatch({ type: "cleared" })}>Clear cart</button>}
</p>
</div>
);
}Every possible change to the cart is visible in cartReducer. The component only describes events.
Reducers must be pure#
Like components, reducers must be pure: no mutation of the old state, no random values, no requests or timers. They only calculate the next state from the current state and the action. Generate ids or timestamps in the event handler and put them in the action.
Because reducers are plain functions, they are easy to test without rendering anything:
const next = cartReducer([], { type: "added", product: { id: "tea", name: "Tea", price: 180 } });
// next → [{ id: "tea", name: "Tea", price: 180, qty: 1 }]Conventions#
- Name action types after what happened (
"added","task_deleted") rather than after a setter ("setItems"). - Use a
switchwith a block{ }around cases that declare variables. - Throw on unknown action types, so typos are caught immediately.
useState or useReducer?#
Both are fine; you can switch from one to the other at any time. Many components use several useState calls and never need a reducer.
Combining with context#
A reducer's state and dispatch are often provided through context, so deeply nested components can read the state and dispatch actions without passing props through every level. That pattern is a lightweight alternative to state-management libraries.
Common mistakes#
Practice#
Practice exercise
Write a reducer for a quiz score with actions "correct" (+10 points, and increase the streak), "wrong" (−5 points but not below 0, and reset the streak) and "reset". Show the score and streak with three buttons.
Show hint
State: { score: 0, streak: 0 }. For "wrong": { score: Math.max(0, state.score - 5), streak: 0 }.
Show solution
import { useReducer } from "react";
const initial = { score: 0, streak: 0 };
function quizReducer(state, action) {
switch (action.type) {
case "correct":
return { score: state.score + 10, streak: state.streak + 1 };
case "wrong":
return { score: Math.max(0, state.score - 5), streak: 0 };
case "reset":
return initial;
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
export default function App() {
const [state, dispatch] = useReducer(quizReducer, initial);
return (
<div>
<p>Score: {state.score} · Streak: {state.streak} {state.streak >= 3 && "🔥"}</p>
<button onClick={() => dispatch({ type: "correct" })}>Correct</button>{" "}
<button onClick={() => dispatch({ type: "wrong" })}>Wrong</button>{" "}
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}Key takeaway#
Next: Context: Passing Data Deeply.
Finished this lesson?