¿Cuándo usarías `useReducer` en lugar de `useState`?
Ver respuesta — intenta responderla en voz alta primero
Uso useReducer cuando el estado es complejo y tiene varias transiciones relacionadas entre sí: en vez de repartir la lógica entre muchos setState, la concentro en un reducer puro que recibe acciones tipadas. Para valores simples e independientes, useState sigue siendo la mejor opción.
useState funciona muy bien hasta que un mismo dato se puede modificar de muchas formas distintas (agregar, quitar, vaciar, reiniciar...) y la lógica termina dispersa por todos los handlers. useReducer le da estructura:
- Defines las acciones posibles como una unión discriminada de TypeScript: cada acción declara su
typey su payload. - Escribes un reducer puro: recibe el estado actual y una acción, y devuelve el estado nuevo sin mutar nada. Al ser puro, es trivial de testear sin renderizar.
- El componente solo hace
dispatch({ type: "..." }); el "cómo" vive en un solo lugar.
Comparación honesta: useReducer no es "mejor", es más ceremonioso. Para un booleano de un modal o el texto de un input, useState es más claro. El reducer paga su costo cuando hay tres o más transiciones que tocan el mismo estado, o cuando el estado nuevo depende del anterior de formas no triviales.
import { useReducer } from "react";
type Producto = { id: number; nombre: string };
// Unión discriminada: cada acción declara su propio payload
type Accion =
| { type: "agregar"; producto: Producto }
| { type: "quitar"; id: number }
| { type: "vaciar" };
// Reducer puro: mismo estado + misma acción = mismo resultado
function reducerCarrito(estado: Producto[], accion: Accion): Producto[] {
switch (accion.type) {
case "agregar":
return [...estado, accion.producto]; // TS sabe que aquí hay producto
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>
);
}
Mutar el estado dentro del reducer. React compara referencias: si devuelves el mismo array modificado, no detecta el cambio y no re-renderiza.
// MAL: push muta el array original y devuelve la misma referencia
case "agregar":
estado.push(accion.producto);
return estado;
// BIEN: devolver un array NUEVO con spread
case "agregar":
return [...estado, accion.producto];
Uso
useReducercuando un estado tiene varias transiciones relacionadas, como un carrito donde puedo agregar, quitar y vaciar. En vez de repartir esa lógica entre variossetState, la concentro en un reducer puro, y tipo las acciones como una unión discriminada para que TypeScript valide cada payload. Eso me da una lógica centralizada y fácil de testear, porque el reducer es una función pura. Para estado simple, como un booleano o un input, me quedo conuseState: ahí el reducer solo agrega ceremonia.
Crea un contador con useReducer que soporte dos acciones: incrementar (con una cantidad como payload) y reiniciar.
Ver solución
import { useReducer } from "react";
type Accion = { type: "incrementar"; cantidad: number } | { type: "reiniciar" };
// Reducer puro con la unión discriminada de acciones
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>
);
}