← React & TypeScript

Estado y patrones

¿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 type y 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 useReducer cuando un estado tiene varias transiciones relacionadas, como un carrito donde puedo agregar, quitar y vaciar. En vez de repartir esa lógica entre varios setState, 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 con useState: ahí el reducer solo agrega ceremonia.

Reto rápido

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>
  );
}