import { useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return { count: 0 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
</>
);
}- reducer function: takes current state + an action, returns new state โ pure, predictable logic, all in one place
- action object: describes what happened, using a
typefield by convention - dispatch: the function you call to trigger a state transition
For simple, independent values (a toggle, a text input), useState is simpler and more direct. Reach for useReducer when: several state values update together in response to the same action, the next state genuinely depends on the previous one in a complex way, or you have many distinct ways state can change (making a growing pile of separate useState calls and handler functions hard to follow).
function todoReducer(state, action) {
switch (action.type) {
case "add":
return [...state, { id: Date.now(), text: action.text, done: false }];
case "toggle":
return state.map((todo) =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
);
case "remove":
return state.filter((todo) => todo.id !== action.id);
default:
return state;
}
}
const [todos, dispatch] = useReducer(todoReducer, []);
dispatch({ type: "add", text: "Learn useReducer" });Notice all the todo-updating logic (add, toggle, remove) lives in one place โ the reducer function โ rather than scattered across several separate handler functions in the component itself.
The same immutability rule from Chapter 13 applies inside a reducer: always return a new state object or array, never mutate the existing state parameter directly. A reducer that mutates and returns the same reference can cause React to miss the update entirely.
Build a simple like/dislike counter using useReducer with three action types: like, dislike, and reset.
import { useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "like": return { count: state.count + 1 };
case "dislike": return { count: state.count - 1 };
case "reset": return { count: 0 };
default: return state;
}
}
function LikeButton() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: "like" })}>๐</button>
<button onClick={() => dispatch({ type: "dislike" })}>๐</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</>
);
}
Q Is useReducer a replacement for Redux?
Not exactly โ useReducer manages state within a single component (or a small tree via Context, covered next chapter), while Redux (Chapter 25) manages global state across an entire app with additional tooling like middleware and dev tools.
Q Why is the action object usually shaped with a 'type' field?
It's a strong convention (not a hard requirement) that makes reducers easy to read and predictable โ the reducer's switch statement branches directly on action.type, making every possible state transition explicit and easy to trace.