When would you use `useReducer` instead of `useState`?
Show answer — try answering out loud first
I use useReducer when the state is complex and has several transitions related to each other: instead of spreading the logic across many setState calls, I concentrate it in a pure reducer that receives typed actions. For simple, independent values, useState is still the best option.
useState works very well until the same piece of data can be modified in many different ways (add, remove, clear, reset...) and the logic ends up scattered across all the handlers. useReducer gives it structure:
- You define the possible actions as a TypeScript discriminated union: each action declares its
typeand its payload. - You write a pure reducer: it receives the current state and an action, and returns the new state without mutating anything. Being pure, it's trivial to test without rendering.
- The component only calls
dispatch({ type: "..." }); the "how" lives in a single place.
An honest comparison: useReducer isn't "better", it's more ceremonious. For a modal's boolean or an input's text, useState is clearer. The reducer pays off when there are three or more transitions that touch the same state, or when the new state depends on the previous one in non-trivial ways.
import { useReducer } from "react";
type Producto = { id: number; nombre: string };
// Discriminated union: each action declares its own payload
type Accion =
| { type: "agregar"; producto: Producto }
| { type: "quitar"; id: number }
| { type: "vaciar" };
// Pure reducer: same state + same action = same result
function reducerCarrito(estado: Producto[], accion: Accion): Producto[] {
switch (accion.type) {
case "agregar":
return [...estado, accion.producto]; // TS knows there's a product here
case "quitar":
return estado.filter((producto) => producto.id !== accion.id);
case "vaciar":
return [];
}
}
export function Carrito() {
const [productos, dispatch] = useReducer(reducerCarrito, []);
const manzana: Producto = { id: productos.length + 1, nombre: "Manzana" };
return (
<div>
<p>Productos en el carrito: {productos.length}</p>
<button onClick={() => dispatch({ type: "agregar", producto: manzana })}>Agregar</button>
<button onClick={() => dispatch({ type: "vaciar" })}>Vaciar</button>
</div>
);
}
Mutating the state inside the reducer. React compares references: if you return the same modified array, it doesn't detect the change and doesn't re-render.
// BAD: push mutates the original array and returns the same reference
case "agregar":
estado.push(accion.producto);
return estado;
// GOOD: return a NEW array with spread
case "agregar":
return [...estado, accion.producto];
I use
useReducerwhen a state has several related transitions, like a cart where I can add, remove, and clear. Instead of spreading that logic across severalsetStatecalls, I concentrate it in a pure reducer, and I type the actions as a discriminated union so TypeScript validates each payload. That gives me centralized, easy-to-test logic, because the reducer is a pure function. For simple state, like a boolean or an input, I stick withuseState: there the reducer only adds ceremony.
Create a counter with useReducer that supports two actions: incrementar (with a cantidad as payload) and reiniciar.
See solution
import { useReducer } from "react";
type Accion = { type: "incrementar"; cantidad: number } | { type: "reiniciar" };
// Pure reducer with the discriminated union of actions
function reducerContador(estado: number, accion: Accion): number {
switch (accion.type) {
case "incrementar":
return estado + accion.cantidad;
case "reiniciar":
return 0;
}
}
export function Contador() {
const [cuenta, dispatch] = useReducer(reducerContador, 0);
return (
<div>
<p>Cuenta: {cuenta}</p>
<button onClick={() => dispatch({ type: "incrementar", cantidad: 5 })}>+5</button>
<button onClick={() => dispatch({ type: "reiniciar" })}>Reiniciar</button>
</div>
);
}