Hook useReducer
Gestiona transiciones de estado complejas con una función reductora y acciones despachadas.
Receta
Tarjeta de referencia rápida — lista para copiar y pegar.
const [state, dispatch] = useReducer(reducer, initialState);
// Con inicializador perezoso
const [state, dispatch] = useReducer(reducer, initialArg, init);
// Despachar una acción
dispatch({ type: "increment" });
dispatch({ type: "setName", payload: "Alice" });Cuándo usarlo: Tu estado tiene múltiples sub-valores, las transiciones dependen del estado anterior, o deseas centralizar la lógica de estado para capacidad de prueba.
Ejemplo Funcional
"use client";
import { useReducer } from "react";
type State = { count: number; step: number };
type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "setStep"; payload: number }
| { type: "reset" };
const initialState: State = { count: 0, step: 1 };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment":
return { ...state, count: state.count + state.step };
case "decrement":
return { ...state, count: state.count - state.step };
case "setStep":
return { ...state, step: action.payload };
case "reset":
return initialState;
default:
return state;
}
}
export function StepCounter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div className="space-y-3">
<div className="flex items-center gap-4">
<button onClick={() => dispatch({ type: "decrement" })} className="px-3 py-1 border rounded">
−
</button>
<span className="text-xl font-mono w-16 text-center">{state.count}</span>
<button onClick={() => dispatch({ type: "increment" })} className="px-3 py-1 border rounded">
+
</button>
</div>
<label className="flex items-center gap-2 text-sm">
Paso:
<input
type="number"
value={state.step}
onChange={(e) => dispatch({ type: "setStep", payload: Number(e.target.value) })}
className="w-16 border rounded px-2 py-1"
/>
</label>
<button onClick={() => dispatch({ type: "reset" })} className="text-sm text-blue-600 underline">
Reiniciar
</button>
</div>
);
}Lo que esto demuestra:
- Un reductora gestiona múltiples valores de estado relacionados (
countystep) - Tipos de unión discriminada para acciones type-safe
- El reductora es una función pura — fácil de probar de forma aislada
dispatches estable entre renderizados y seguro de pasar a componentes hijos sinuseCallback
Inmersión Profunda
Cómo Funciona
useReduceracepta una función reductora pura(state, action) => newStatey un estado inicial- Llamar a
dispatch(action)envía la acción a través del reductora y provoca un re-renderizado con el nuevo estado - React garantiza que la identidad de
dispatches estable — nunca cambia entre renderizados - Al igual que
useState, React agrupa múltiples envíos dentro del mismo manejador de evento en un único re-renderizado - El reductora se ejecuta durante el renderizado, por lo que debe ser puro — sin efectos secundarios, sin mutaciones
Parámetros y Valores de Retorno
| Parámetro | Tipo | Descripción |
|---|---|---|
reducer | (state: S, action: A) => S | Función pura que calcula el nuevo estado a partir del estado actual y la acción |
initialArg | S o I | Estado inicial, o argumento pasado a la función init |
init | (initialArg: I) => S | Función inicializadora perezosa opcional |
| Retorno | Tipo | Descripción |
|---|---|---|
state | S | Valor de estado actual |
dispatch | (action: A) => void | Función para enviar acciones al reductora |
Variaciones
Con inicializador perezoso:
function init(initialCount: number): State {
return { count: initialCount, step: 1 };
}
const [state, dispatch] = useReducer(reducer, 0, init);Reductora con Immer para actualizaciones más limpias:
import { useImmerReducer } from "use-immer";
function reducer(draft: State, action: Action) {
switch (action.type) {
case "addTodo":
draft.todos.push({ id: Date.now(), text: action.payload, done: false });
break;
case "toggleTodo":
const todo = draft.todos.find((t) => t.id === action.payload);
if (todo) todo.done = !todo.done;
break;
}
}Empareja con contexto para estado global:
const StateContext = createContext<State>(initialState);
const DispatchContext = createContext<Dispatch<Action>>(() => {});
export function AppProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}Notas de TypeScript
// Unión discriminada para acciones — TypeScript estrecha dentro de casos switch
type Action =
| { type: "add"; payload: string }
| { type: "remove"; payload: number }
| { type: "clear" };
// useReducer genérico recoge tipos automáticamente
const [state, dispatch] = useReducer(reducer, initialState);
// dispatch se tipifica como Dispatch<Action>Trampa
-
Mutar el estado directamente — Modificar
state.count++dentro del reductora no provocará un re-renderizado y corromperá tu estado. Solución: Siempre retorna un nuevo objeto:{ ...state, count: state.count + 1 }. -
Efectos secundarios en el reductora — Obtener datos o escribir en localStorage dentro del reductora rompe el modelo de renderizado de React. Solución: Mantén el reductora puro; ejecuta efectos secundarios en
useEffecto manejadores de eventos. -
Olvidar el caso predeterminado — Si se despacha una acción no reconocida y no hay un caso predeterminado que retorne
state, obtendrásundefined. Solución: Siempre incluyedefault: return stateen tu switch. -
Exceso de ingeniería del estado simple — Usar
useReducerpara un booleano o número único añade complejidad innecesaria. Solución: UsauseStatepara valores simples e independientes.
Alternativas
| Alternativa | Úsalo Cuando | No lo Uces Cuando |
|---|---|---|
useState | Uno o dos valores de estado independientes | Múltiples valores relacionados con transiciones complejas |
| Zustand | Estado compartido entre muchos componentes con selectores | El estado es local a un árbol de componentes |
| XState | Necesitas máquinas de estado formal con guardias y transiciones | Operaciones CRUD simples |
useActionState (React 19) | Las transiciones de estado están vinculadas a envíos de formularios | Gestión general de estado del lado del cliente |
¿Por qué no siempre usar useReducer? Para un interruptor único o contador, useState es más simple y legible. Utiliza useReducer cuando tengas 3+ valores de estado relacionados o el siguiente estado dependa tanto del estado actual como de la carga de una acción.
FAQs
¿Cuál es la ventaja clave de useReducer sobre múltiples llamadas useState?
useReducercentraliza transiciones de estado relacionadas en una única función pura.- El reductora es fácil de probar de forma aislada -- solo llama a
reducer(state, action)y afirma el resultado. - Previene combinaciones de estado imposibles que pueden ocurrir con llamadas
useStateindependientes.
¿Por qué la función dispatch se garantiza que sea estable entre renderizados?
- React crea la función
dispatchuna vez y retorna la misma referencia en cada renderizado. - Esto significa que puedes pasar
dispatchde manera segura a componentes hijos sinuseCallback. - Los hijos envueltos en
React.memono se re-renderizarán debido a quedispatchcambie.
Trampa: ¿Qué sucede si muto el estado directamente dentro del reductora?
- Mutar
state.count++modifica el objeto existente sin crear una nueva referencia. - React usa
Object.ispara detectar cambios, así que no verá la mutación y no se re-renderizará. - Siempre retorna un nuevo objeto:
{ ...state, count: state.count + 1 }.
¿Puedo ejecutar efectos secundarios (llamadas API, localStorage) dentro de un reductora?
- No. Los reductoras deben ser funciones puras -- sin efectos secundarios, sin operaciones asincrónicas, sin mutaciones.
- El reductora se ejecuta durante el renderizado, por lo que las operaciones impuras rompen el modelo de React.
- Ejecuta efectos secundarios en
useEffecto manejadores de eventos que despachen acciones.
¿Cómo tipico acciones con uniones discriminadas de TypeScript?
type Action =
| { type: "add"; payload: string }
| { type: "remove"; payload: number }
| { type: "clear" };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "add":
// action.payload es string aquí
return { ...state, items: [...state.items, action.payload] };
case "remove":
// action.payload es number aquí
return { ...state, items: state.items.filter((_, i) => i !== action.payload) };
case "clear":
return { ...state, items: [] };
}
}¿Qué es un inicializador perezoso y cuándo debo usarlo con useReducer?
- El tercer argumento a
useReducer(reducer, initialArg, init)es una función que calcula el estado inicial a partir deinitialArg. - Se ejecuta solo una vez al montar, evitando inicialización costosa en cada renderizado.
- Úsalo cuando el estado inicial requiere cálculo, como analizar props o leer desde el almacenamiento.
¿Cómo emparejar useReducer con useContext para estado global?
- Crea dos contextos: uno para el estado, otro para dispatch.
- Proporciona ambos desde un componente padre que llama a
useReducer. - Dividirlos evita que los componentes que solo despachan se re-renderizen cuando cambia el estado.
Trampa: ¿Qué sucede si olvido el caso predeterminado en mi switch del reductora?
- Si se despacha una acción no reconocida y no hay
default: return state, el reductora retornaundefined. - Esto reemplaza todo tu estado con
undefined, rompiendo el componente. - Siempre incluye
default: return statecomo red de seguridad.
¿Cuándo debo usar useReducer vs useState?
- Usa
useStatepara uno o dos valores independientes (un interruptor, un contador). - Usa
useReducercuando tengas 3+ valores relacionados, transiciones complejas, o desees lógica de estado probada. - Si las transiciones de estado dependen tanto del estado actual como de la carga de una acción,
useReduceres más claro.
¿Cómo funciona useReducer con Immer para actualizaciones inmutables más limpias?
import { useImmerReducer } from "use-immer";
function reducer(draft: State, action: Action) {
switch (action.type) {
case "addTodo":
draft.todos.push({ id: Date.now(), text: action.payload, done: false });
break;
case "toggleTodo":
const todo = draft.todos.find(t => t.id === action.payload);
if (todo) todo.done = !todo.done;
break;
}
}- Immer te permite escribir código "mutante" que produce actualizaciones inmutables bajo el capó.
¿Cómo tipico la función dispatch al pasarla a través del contexto en TypeScript?
import { createContext, Dispatch } from "react";
const DispatchContext = createContext<Dispatch<Action>>(() => {});
// Los consumidores obtienen un dispatch tipificado:
// dispatch({ type: "add", payload: "item" }) -- OK
// dispatch({ type: "unknown" }) -- Error de TypeScriptRelacionado
- useState — alternativa más simple para valores de estado independientes
- useContext — empareja con
useReducerpara gestión de estado global - useActionState — hook de React 19 para transiciones de estado impulsadas por formularios
- Custom Hooks — envuelve
useReduceren un hook personalizado para encapsular lógica de dominio