Menu

Hook useReducer en React: sintaxis, ejemplos y vs useState

useReducer guarda estado y lleva cada actualización a una sola función reducer: despachas una acción y el reducer devuelve el siguiente estado. Aprende la sintaxis, cómo escribir un reducer puro, un ejemplo de lista de tareas, la inicialización diferida y cuándo elegirlo en lugar de useState.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

useReducer es un hook de React que guarda estado como useState, pero lleva todas las formas de cambiarlo a una sola función, el reducer. Tu componente llama a dispatch con un objeto de acción como { type: 'increment' }, y React pasa el estado actual y esa acción al reducer, que devuelve el siguiente estado.

Los botones no dicen cómo cambia el contador, solo qué pasó. Toda la aritmética vive en reducer. Agrega un case 'double' que devuelva { count: state.count * 2 } y un botón que lo despache para ver cómo encaja un nuevo tipo de actualización.

La sintaxis

const [state, dispatch] = useReducer(reducer, initialArg, init); // init is optional

function reducer(state, action) {
    // return the next state
}
  • reducer es una función (state, action) => nextState. Defínela fuera del componente; no necesita nada del ámbito del componente.
  • initialArg es el estado inicial, usado solo en el primer renderizado.
  • init es opcional. Si la pasas, el estado inicial es init(initialArg) en su lugar.
  • state es el estado actual para este renderizado.
  • dispatch(action) envía una acción al reducer y programa un renderizado con el resultado. Es estable: es la misma función en cada renderizado, así que puedes pasarla hacia abajo o incluirla como dependencia sin problema.

Una acción puede ser cualquier valor, pero por convención es un objeto con una cadena type que describe qué pasó, más los datos que necesite el reducer: { type: 'added', text: 'Buy milk' }.

Escribir un reducer

La mayoría de los reducers son un switch sobre action.type, con un case por cada tipo de evento. Cada caso devuelve un estado completamente nuevo; nunca cambia el viejo. El caso default lanza un error, así que una errata como dispatch({ type: 'incremnet' }) falla de forma visible en lugar de no hacer nada.

Nombra las acciones según lo que hizo el usuario (added, toggled, deleted), no según el cambio de estado que tienes en mente (setTodos). Así el componente se lee como una lista de eventos, y el reducer es el único lugar que decide qué significa cada evento.

Los reducers deben ser puros

Un reducer es una función pura: con el mismo estado y la misma acción, devuelve el mismo resultado, y no hace nada más. Eso significa nada de mutación, peticiones, temporizadores, ni Math.random() o Date.now() dentro de él. React depende de esto. En desarrollo con StrictMode, React llama a tu reducer dos veces por cada acción y conserva un resultado, para ayudarte a detectar un reducer que no es puro. La vista previa de aquí funciona como un build de producción, así que los ejemplos de esta página lo llaman una vez.

La regla de la mutación es la que más se rompe. Hacer push en state.todos y devolver state devuelve el mismo objeto, así que React no ve ningún cambio y omite el renderizado, la misma trampa descrita en actualizar arrays y objetos. Construye arrays y objetos nuevos con spread, map y filter.

// Wrong: mutates and returns the same object
case 'added':
    state.todos.push(action.todo);
    return state;

// Right: returns a new object with a new array
case 'added':
    return { ...state, todos: [...state.todos, action.todo] };

Los efectos secundarios que pertenecen a un evento, como guardar en un servidor, van en el manejador de eventos junto a dispatch. Los efectos secundarios que pertenecen al estado, como sincronizarlo con localStorage, van en un efecto.

Una lista de tareas con useReducer

Aquí el reducer hace trabajo real: agregar, marcar y borrar tareas. El console.log al principio del reducer imprime cada acción, una forma práctica de ver cómo cambia el estado mientras depuras. Es el único efecto secundario que se suele tolerar en un reducer, porque no cambia nada; quítalo antes de publicar.

Agrega una tarea, marca una casilla y borra una, y luego lee la consola: cada cambio es una línea que nombra la acción y sus datos. El texto del input se queda en useState, porque es local del campo y ningún otro evento lo toca. Mezclar los dos hooks en un componente es normal.

El id de la nueva tarea se crea en el manejador de clic y viaja en la acción, así que el reducer solo lo copia. Escribir id: nextId++ dentro del reducer lo volvería impuro: con StrictMode en desarrollo, la segunda llamada se saltaría un id.

dispatch no cambia el estado de inmediato

dispatch funciona como un setter de useState: programa un renderizado, y la variable state de tu manejador actual conserva el valor viejo. Para usar el nuevo estado enseguida, calcúlalo tú mismo llamando al reducer.

Haz clic varias veces. La primera línea del registro siempre va un paso atrás, y la segunda coincide con el número del botón después del renderizado. Como el reducer es una función normal, llamarlo tú mismo es seguro, otra ventaja de mantenerlo puro.

useState vs useReducer

Ambos hooks guardan estado, y todo lo que escribas con uno lo puedes escribir con el otro. La diferencia es dónde vive la lógica de actualización.

useStateuseReducer
Lógica de actualizaciónEn cada manejador de eventosEn una sola función reducer
Útil paraUnos pocos valores independientesVarios campos que cambian juntos por muchos eventos
Cantidad de códigoMenos para estado simpleMás al principio, menos a medida que crecen los eventos
PruebasProbar a través del componenteProbar el reducer como función normal
DepuraciónBuscar qué manejador lo cambióRegistrar cada acción en un solo lugar

Recurre a useReducer cuando notes que el mismo estado se actualiza en muchos manejadores, cuando un evento tiene que cambiar varias piezas de estado que deben mantenerse coherentes, o cuando un manejador es sobre todo lógica sobre el siguiente estado. Para un interruptor o un campo de texto, useState es más corto y claro. Cambiar después no es difícil: reemplaza los setters por dispatches y mueve la lógica a los casos.

Inicialización diferida

Si escribes el estado inicial como una llamada a función, por ejemplo useReducer(reducer, createInitialState('Ada')), esa llamada se ejecuta en cada renderizado, aunque React solo usa su resultado la primera vez. Si construir el estado inicial es costoso, o debe derivarse de una prop, pasa un tercer argumento: una función init. React llama a init(initialArg) una vez, en el primer renderizado.

Escribe en el input: el componente renderiza en cada pulsación, pero createInitialState registra solo una vez. Ahora cambia la llamada a useReducer(reducer, createInitialState('Ada')) y vuelve a escribir. La función se ejecuta en cada renderizado, y React descarta el resultado cada vez después de la primera.

Pasa la función en sí, no el resultado de llamarla. useReducer(reducer, 'Ada', createInitialState) es diferido; useReducer(reducer, createInitialState('Ada')) no lo es.

useReducer con contexto

Un reducer combina bien con el contexto cuando un árbol profundo necesita el estado. Pon el estado y dispatch en contexto arriba, y cualquier componente de abajo puede leer el estado o enviar acciones sin props que pasen por cada capa. Como dispatch nunca cambia, los componentes que solo envían acciones pueden leer un contexto de dispatch aparte y evitar volver a renderizarse cuando cambia el estado.

import { createContext, useContext, useReducer } from 'react';

const TodosContext = createContext(null);
const TodosDispatchContext = createContext(null);

export function TodosProvider({ children }) {
    const [todos, dispatch] = useReducer(todosReducer, []);
    return (
        <TodosContext value={todos}>
            <TodosDispatchContext value={dispatch}>{children}</TodosDispatchContext>
        </TodosContext>
    );
}

function AddTodo() {
    const dispatch = useContext(TodosDispatchContext);
    return <button onClick={() => dispatch({ type: 'added', text: 'New' })}>Add</button>;
}

En React 19 un objeto de contexto funciona como su propio proveedor (<TodosContext value={...}>); <TodosContext.Provider> también sigue funcionando. La página de useContext cubre cómo llega el contexto a los componentes y cuándo los vuelve a renderizar.

Preguntas frecuentes

¿Qué es useReducer en React?

Un hook para estado cuyas actualizaciones se describen como acciones. Llamas a const [state, dispatch] = useReducer(reducer, initialState) y luego a dispatch({ type: 'added' }) desde los manejadores de eventos. React pasa el estado actual y la acción a tu reducer, y lo que devuelva se convierte en el siguiente estado.

¿Cuándo debo usar useReducer en lugar de useState?

Cuando varios eventos actualizan el mismo estado de formas relacionadas, cuando el siguiente estado depende de varios campos a la vez, o cuando la lógica de actualización es tan larga que la quieres en una sola función comprobable fuera del componente. Para uno o dos valores independientes, useState es más sencillo.

¿Por qué un reducer debe ser puro?

React puede llamar a tu reducer más de una vez para la misma acción (StrictMode lo hace a propósito en desarrollo) y espera el mismo resultado cada vez. Así que un reducer no debe mutar el estado, obtener datos, crear temporizadores ni leer valores aleatorios. Haz esas cosas en manejadores de eventos o en efectos.

¿dispatch actualiza el estado de inmediato?

No. Igual que un setter de useState, dispatch programa un renderizado. Dentro del manejador de eventos actual state sigue teniendo el valor viejo; el nuevo estado aparece en el siguiente renderizado.

¿Cuál es el tercer argumento de useReducer?

Una función init opcional. Cuando la pasas, React calcula el estado inicial como init(initialArg), y solo en el primer renderizado. Es útil cuando construir el estado inicial es costoso o depende de una prop.

Ilustración de los lenguajes de programación de Coddy

Aprende a programar con Coddy

COMENZAR