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:
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 / 255Sin 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?