Saltar al contenido

useReducer: el estado como máquina

¿Cuándo useReducer le gana a useState, y por qué?

Al terminar vas a poder explicar

  • Explicar por qué useState y useReducer comparten implementación
  • Decidir entre los dos con un criterio, no por gusto
  • Escribir un reducer que haga imposibles los estados inválidos

Se suele presentar useReducer como «useState para estado complicado». Esa descripción no explica nada. La verdad es más interesante y más útil:

useState ES useReducer. Con un reducer trivial.

No es una analogía. En el código de React, literalmente:

ReactFiberHooks, simplificado
1function basicStateReducer(state, action) {2  return typeof action === "function" ? action(state) : action;3}4 5function updateState(initialState) {6  return updateReducer(basicStateReducer, initialState);7}

Ahí está el misterio entero. Cuando llamás a setN(5), el reducer devuelve 5. Cuando llamás a setN(v => v + 1), el reducer detecta que la acción es una función y la ejecuta con el estado actual. La función actualizadora no es una característica especial de useState: es el reducer por defecto haciendo su trabajo.

Entonces, ¿qué cambia de verdad?

Por dentro, nada: la misma cadena de hooks, la misma cola circular, el mismo procesamiento por prioridad. Lo que cambia es dónde vive la lógica de transición.

  • Con useState, cada componente que quiera cambiar el estado tiene que saber cómo calcular el estado siguiente.
  • Con useReducer, los componentes declaran qué pasó y el reducer decide qué significa.

Tres acciones despachadas: mirá la cola y el reducer

AGENDADOpaso 1 / 255
Traza
Cadena de hooks · <>

Sin hooks en este paso.

Árbol de fibers

  • <HostRoot>

Salida

Nada en el host todavía. React no commiteó ningún cambio.

createRoot().render()

Montaje inicial. No hay árbol previo, así que todo se va a crear desde cero.

VER EL CÓDIGO QUE SE ESTÁ TRAZANDO
1function carritoReducer(estado, accion) {2  switch (accion.tipo) {3    case "agregar":4      return { items: estado.items + 1, total: estado.total + accion.precio };5    case "vaciar":6      return { items: 0, total: 0 };7  }8}9 10function Carrito() {11  const [estado, despachar] = useReducer(carritoReducer, { items: 0, total: 0 });12 13  return (14    <div>15      <p>{estado.items} items · ${estado.total}</p>16      <button onClick={() => despachar({ tipo: "agregar", precio: 250 })}>agregar</button>17      <button onClick={() => despachar({ tipo: "vaciar" })}>vaciar</button>18    </div>19  );20}

Con el foco acá: ← → un paso · espacio reproduce · Inicio/Fin van a los extremos.

En la traza vas a ver exactamente lo mismo que en useState: processUpdateQueue, los updates aplicándose en orden, uno recibiendo el resultado del anterior. Porque es el mismo código.

El argumento de verdad: estados imposibles

El beneficio real de un reducer no es «organizar». Es que te permite hacer que ciertos estados no puedan existir.

1// Con useState suelto: 2³ = 8 combinaciones posibles.2// Varias son absurdas — ¿cargando Y con error Y con datos?3const [cargando, setCargando] = useState(false);4const [error, setError] = useState(null);5const [datos, setDatos] = useState(null);6 7// Con un reducer: exactamente 4 estados, y son los 4 que existen.8function reducer(estado, accion) {9  switch (accion.tipo) {10    case "pedir":   return { fase: "cargando" };11    case "listo":   return { fase: "exito", datos: accion.datos };12    case "fallo":   return { fase: "error", error: accion.error };13    case "reiniciar": return { fase: "vacio" };14  }15}

Con la primera versión vas a escribir setCargando(false) en cinco lugares y olvidarte en uno. Con la segunda, la transición está en un solo lugar y es imposible quedar a mitad de camino.

Un reducer es una función pura que podés probar sola

Esto es lo que más se subestima. Un reducer no depende de React: recibe un estado y una acción, devuelve un estado. Lo probás sin renderizar nada, sin librería de testing de componentes, sin DOM.

1test("agregar suma el precio al total", () => {2  const estado = { items: 1, total: 250 };3  const siguiente = carrito(estado, { tipo: "agregar", precio: 100 });4 5  expect(siguiente).toEqual({ items: 2, total: 350 });6  expect(estado).toEqual({ items: 1, total: 250 });  // no lo mutó7});

Toda la lógica de negocio de tu pantalla, verificada en milisegundos y sin montar un componente. Ese es el argumento fuerte, no la organización.

Antes de seguir · predecí

¿Cuál de estas afirmaciones sobre useReducer es cierta?

Lo que te llevás

useState es useReducer con basicStateReducer. Elegir el reducer no es por rendimiento ni por cantidad de estado: es para poner la transición en un solo lugar, hacer imposibles los estados absurdos y poder probar la lógica sin React.

Antes de marcarla, comprobá

  • ¿Podés explicar explicar por qué useState y useReducer comparten implementación?
  • ¿Podés explicar decidir entre los dos con un criterio, no por gusto?
  • ¿Podés explicar escribir un reducer que haga imposibles los estados inválidos?