useReducer
Cuando el estado de un componente tiene varios campos que cambian juntos, o la lógica de “cómo cambia” es más compleja que un setState, tener 5 useState sueltos empieza a sentirse desordenado — es fácil actualizar uno y olvidarse de otro relacionado. useReducer centraliza esa lógica en una sola función pura: dado el estado actual y una acción, devuelve el estado siguiente. El componente ya no decide cómo cambia el estado, solo qué pasó (dispatch({ type: '...' })).
Integración básica
Sección titulada «Integración básica»import { useReducer } from "react"
interface State { nombre: string edad: number}
type Action = { type: "incrementar_edad" } | { type: "cambiar_nombre"; nombre: string }
function reducer(state: State, action: Action): State { switch (action.type) { case "incrementar_edad": return { ...state, edad: state.edad + 1 } case "cambiar_nombre": return { ...state, nombre: action.nombre } }}
function Perfil() { const [state, dispatch] = useReducer(reducer, { nombre: "Ana", edad: 30 })
return ( <> <p> {state.nombre}, {state.edad} años </p> <button onClick={() => dispatch({ type: "incrementar_edad" })}> Cumplir años </button> </> )}Acciones con payload — discriminated union
Sección titulada «Acciones con payload — discriminated union»El truco está en el tipo Action: cada variante tiene su propio type como valor literal, más los campos que esa acción específica necesita. TypeScript usa el type como discriminante — dentro de cada case, action ya está tipado con solo los campos de esa variante, sin castear nada a mano.
type Action = | { type: "agregar_item"; item: string } | { type: "quitar_item"; id: string } | { type: "actualizar_cantidad"; id: string; cantidad: number } | { type: "vaciar_carrito" }
function reducer(state: CarritoState, action: Action): CarritoState { switch (action.type) { case "agregar_item": // aquí action.item existe y está tipado — action.id no existiría return { ...state, items: [...state.items, action.item] } case "quitar_item": return { ...state, items: state.items.filter((i) => i.id !== action.id) } case "actualizar_cantidad": return { ...state, items: state.items.map((i) => i.id === action.id ? { ...i, cantidad: action.cantidad } : i ) } case "vaciar_carrito": return { ...state, items: [] } }}TypeScript también avisa si te olvidaste de manejar una variante del tipo Action en el switch — con strict activado y sin default, un case faltante rompe la verificación de que la función devuelve State en todos los caminos.
Inicialización perezosa (tercer argumento)
Sección titulada «Inicialización perezosa (tercer argumento)»Si el estado inicial es costoso de calcular, un tercer argumento (init) evita recalcularlo en cada render — a diferencia de pasar el resultado directo, que sí se ejecutaría de nuevo cada vez que el componente renderiza.
function crearEstadoInicial(nombreUsuario: string): State { return { nombre: nombreUsuario, edad: 0 /* ... cálculo costoso ... */ }}
const [state, dispatch] = useReducer(reducer, nombreUsuario, crearEstadoInicial)Contrato del reducer en una mirada
Sección titulada «Contrato del reducer en una mirada»| Pieza | Qué es |
|---|---|
reducer(state, action) | Función pura: recibe estado + acción, devuelve el estado siguiente |
dispatch({ type, ...payload }) | Como se “pide” un cambio, nunca se muta el estado directo |
type Action = {...} | {...} | Discriminated union: cada acción con su propio type y payload tipado |
Tercer argumento de useReducer | Función de inicialización perezosa, para estado inicial costoso |
Modelado de acciones y estado
Sección titulada «Modelado de acciones y estado»- El reducer tiene que ser una función pura: nada de
fetch,Math.random()ni mutarstatedirecto — siempre devolver un objeto nuevo. - Sin un
casepara alguna acción y sindefault, TypeScript puede avisar si elswitchno cubre todos los casos. Déjalo así: es la red de seguridad que justifica escribir la unión discriminada. useReducerno es “mejor” queuseStatepor definición — para 1-2 campos independientes,useStatesigue siendo más simple. Vale la pena cuando varios campos cambian juntos con lógica compartida.