useReducer
useReducer
es un Hook de React que te permite agregar un reducer a tu componente.
const [state, dispatch] = useReducer(reducer, initialArg, init?)
- Referencia
- Uso
- Solución de problemas
- He despachado una acción, pero el registro me da el valor de estado antiguo
- He despachado una acción, pero la pantalla no se actualiza
- Una parte del estado de mi reductor se vuelve undefined después de despachar
- Todo el estado de mi reducer se vuelve undefined después de despachar
- Recibo un error: «Too many re-renders»
- Mi función reductora o inicializadora se ejecuta dos veces
Referencia
useReducer(reducer, initialArg, init?)
Llama a useReducer
en el nivel superior de tu componente para gestionar su estado con un reducer.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
Parámetros
reducer
: La función reductora que debe devolver el estado inicial. Debe ser pura, debe tomar el estado y la acción como argumentos, y debe devolver el siguiente estado. El estado y la acción pueden ser de cualquier tipo.initialArg
: El valor a partir del cual se calcula el estado inicial. Puede ser un valor de cualquier tipo. Cómo se calcula el estado inicial depende del siguiente argumentoinit
. opcionalinit
: La función inicializadora que especifica cómo se calcula el estado inicial. Si no se especifica, el estado inicial se establece eninitialArg
. En caso contrario, el estado inicial es el resultado de llamar ainit(initialArg)
.
Devuelve
useReducer
devuelve un array con exactamente dos valores:
- El estado actual. Durante el primer renderizado, se establece a
init(initialArg)
oinitialArg
(si no hayinit
). - La función
dispatch
que permite actualizar el estado a un valor diferente y activar una nueva renderización.
Advertencias
useReducer
es un Hook, por lo que sólo puedes llamarlo en el nivel superior de tu componente o en tus propios Hooks. No puedes llamarlo dentro de bucles o condiciones. Si lo necesitas, extrae un nuevo componente y mueve el estado a él.- La función
dispatch
tiene una identidad estable, por lo que a menudo verás que se omite de las dependencias de los Efectos, pero que se incluya no causa que el Efecto se dispare. Si el linter te permite omitir una dependencia sin errores, es seguro hacerlo. Aprende más sobre eliminar dependencias de los Efectos. - En Modo Estricto, React llamará a tu reducer e inicializador dos veces para ayudarte a encontrar impurezas accidentales Este es un comportamiento sólo de desarrollo y no afecta a la producción. Si tu reducer e inicializador son puros (como deberían ser), esto no debería afectar tu lógica. El resultado de una de las llamadas se ignora.
función dispatch
La función dispatch
devuelta por useReducer
te permite actualizar el estado a un valor diferente y activar una nueva renderización. Es necesario pasar la acción como único argumento a la función dispatch
:
const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
// ...
React establecerá el siguiente estado al resultado de llamar a la función reducer
que has proporcionado con el state
actual y la acción que has pasado a dispatch
.
Parámetros
action
: La acción realizada por el usuario. Puede ser un valor de cualquier tipo. Por convención, una acción suele ser un objeto con una propiedadtype
que lo identifica y, opcionalmente, otras propiedades con información adicional.
Devuelve
Las funciones dispatch
no tienen un valor de devolución.
Advertencias
-
La función
dispatch
sólo actualiza la variable de estado para el siguiente renderizado. Si lees la variable de estado después de llamar a la funcióndispatch
, seguirás obteniendo el valor antiguo que estaba en la pantalla antes de la llamada. -
Si el nuevo valor que proporcionas es idéntico al
state
actual, determinado por una comparaciónObject.is
, React saltará el renderizado del componente y sus hijos. Esto es una optimización. React aún puede necesitar llamar a tu componente antes de ignorar el resultado, pero no debería afectar a tu código. -
React agrupa las actualizaciones de estado. Actualiza la pantalla después de que todos los controladores de eventos se hayan ejecutado y hayan llamado a sus funciones
set
. Esto evita múltiples rerenderizados durante un único evento. En el raro caso de que necesites forzar a React a actualizar la pantalla antes, por ejemplo para acceder al DOM, puedes usarflushSync
.
Uso
Agregar un reducer a un componente
Invoca useReducer
en la parte superior de tu componente para manejar el estado con un reducer.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
useReducer
devuelve un array con exactamente dos elementos:
- El estado actual de esta variable de estado, inicialmente asignado al estado inicial que proporcionaste.
- La función
dispatch
que te permite cambiarlo en respuesta a la interacción.
Para actualizar lo que aparece en pantalla, llama a dispatch
con un objeto que representa lo que hizo el usuario, llamado acción:
function handleClick() {
dispatch({ type: 'incremented_age' });
}
React pasará el estado actual y la acción a tu función reducer. Tu reducer calculará y devolverá el siguiente estado. React almacenará ese siguiente estado, renderizará tu componente con él y actualizará la UI.
import { useReducer } from 'react'; function reducer(state, action) { if (action.type === 'incremented_age') { return { age: state.age + 1 }; } throw Error('Unknown action.'); } export default function Counter() { const [state, dispatch] = useReducer(reducer, { age: 42 }); return ( <> <button onClick={() => { dispatch({ type: 'incremented_age' }) }}> Incrementar edad </button> <p>¡Hola! Tú tienes {state.age}.</p> </> ); }
useReducer
es muy similar a useState
, pero te permite mover la lógica de actualización de estado de los controladores de eventos a una única función fuera de tu componente. Más información sobre elegir entre useState
y useReducer
.
Escribir la función reducer
Una función reducer se declara así:
function reducer(state, action) {
// ...
}
Luego hay que completar el código que calculará y devolverá el siguiente estado. Por convención, es común escribirlo como una declaración switch
. Para cada case
en el switch
, calcula y devuelve un estado siguiente.
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
}
throw Error('Unknown action: ' + action.type);
}
Las acciones pueden tener cualquier forma. Por convención, es común pasar objetos con una propiedad type
que identifica la acción. Debe incluir la información mínima necesaria que el reducer necesita para calcular el siguiente estado.
function Form() {
const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });
function handleButtonClick() {
dispatch({ type: 'incremented_age' });
}
function handleInputChange(e) {
dispatch({
type: 'changed_name',
nextName: e.target.value
});
}
// ...
Los nombres de los tipos de acción son locales a tu componente. Cada acción describe una única interacción, aunque provoque múltiples cambios en los datos. La forma del estado es arbitraria, pero normalmente será un objeto o un array.
Lee extrayendo lógica de estado en un reducer para saber más.
Ejemplo 1 de 3: Form (object)
En este ejemplo, el reducer gestiona un objeto de estado con dos campos: name
y age
.
import { useReducer } from 'react'; function reducer(state, action) { switch (action.type) { case 'incremented_age': { return { name: state.name, age: state.age + 1 }; } case 'changed_name': { return { name: action.nextName, age: state.age }; } } throw Error('Unknown action: ' + action.type); } const initialState = { name: 'Taylor', age: 42 }; export default function Form() { const [state, dispatch] = useReducer(reducer, initialState); function handleButtonClick() { dispatch({ type: 'incremented_age' }); } function handleInputChange(e) { dispatch({ type: 'changed_name', nextName: e.target.value }); } return ( <> <input value={state.name} onChange={handleInputChange} /> <button onClick={handleButtonClick}> Incrementar edad </button> <p>Hola, {state.name}. Tú tienes {state.age}.</p> </> ); }
Evitar recrear el estado inicial
React guarda el estado inicial una vez y lo ignora en las siguientes renderizaciones.
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
// ...
Aunque el resultado de createInitialState(username)
sólo se utiliza para el renderizado inicial, sigues llamando a esta función en cada renderizado. Esto puede ser un desperdicio si está creando grandes arrays o realizando cálculos costosos.
Para solucionar esto, puedes pasarlo como una función initializer a useReducer
como tercer argumento en su lugar:
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...
Fíjate que estás pasando createInitialState
, que es la función en sí, y no createInitialState()
, que es el resultado de llamarla. De esta manera, el estado inicial no se vuelve a crear después de la inicialización.
En el ejemplo anterior, createInitialState
toma un argumento username
. Si tu inicializador no necesita ninguna información para calcular el estado inicial, puedes pasar null
como segundo argumento a useReducer
.
Ejemplo 1 de 2: Pasar la función inicializadora
Este ejemplo pasa la función inicializadora, por lo que la función createInitialState
sólo se ejecuta durante la inicialización. No se ejecuta cuando el componente se vuelve a renderizar, como cuando se escribe en la entrada.
import { useReducer } from 'react'; function createInitialState(username) { const initialTodos = []; for (let i = 0; i < 50; i++) { initialTodos.push({ id: i, text: username + "'s task #" + (i + 1) }); } return { draft: '', todos: initialTodos, }; } function reducer(state, action) { switch (action.type) { case 'changed_draft': { return { draft: action.nextDraft, todos: state.todos, }; }; case 'added_todo': { return { draft: '', todos: [{ id: state.todos.length, text: state.draft }, ...state.todos] } } } throw Error('Unknown action: ' + action.type); } export default function TodoList({ username }) { const [state, dispatch] = useReducer( reducer, username, createInitialState ); return ( <> <input value={state.draft} onChange={e => { dispatch({ type: 'changed_draft', nextDraft: e.target.value }) }} /> <button onClick={() => { dispatch({ type: 'added_todo' }); }}>Agregar</button> <ul> {state.todos.map(item => ( <li key={item.id}> {item.text} </li> ))} </ul> </> ); }
Solución de problemas
He despachado una acción, pero el registro me da el valor de estado antiguo
Llamar a la función dispatch
no cambia el estado del código en ejecución:
function handleClick() {
console.log(state.age); // 42
dispatch({ type: 'incremented_age' }); // Request a re-render with 43
console.log(state.age); // Still 42!
setTimeout(() => {
console.log(state.age); // Also 42!
}, 5000);
}
Esto se debe a que [el estado se comporta como una instantánea] (/learn/state-as-a-snapshot) La actualización del estado solicita otra renderización con el nuevo valor de estado, pero no afecta a la variable JavaScript state
en su controlador de evento ya en ejecución.
Si necesitas averiguar el valor del siguiente estado, puedes calcularlo manualmente llamando tú mismo al reducer:
const action = { type: 'incremented_age' };
dispatch(action);
const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }
He despachado una acción, pero la pantalla no se actualiza
React ignorará tu actualización si el siguiente estado es igual al anterior, determinado por una comparación Object.is
. Esto suele ocurrir cuando cambias un objeto o un array de estado directamente:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Wrong: mutating existing object
state.age++;
return state;
}
case 'changed_name': {
// 🚩 Wrong: mutating existing object
state.name = action.nextName;
return state;
}
// ...
}
}
Has mutado un objeto state
existente y lo has devuelto, por lo que React ha ignorado la actualización. Para solucionarlo, tienes que asegurarte de que siempre estás actualizando objetos en el estado y actualizando arrays en el estado en lugar de mutarlos:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Correct: creating a new object
return {
...state,
age: state.age + 1
};
}
case 'changed_name': {
// ✅ Correct: creating a new object
return {
...state,
name: action.nextName
};
}
// ...
}
}
Una parte del estado de mi reductor se vuelve undefined después de despachar
Asegúrate de que cada rama case
copia todos los campos existentes al devolver el nuevo estado:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state, // Don't forget this!
age: state.age + 1
};
}
// ...
Sin el ...state
de arriba, el siguiente estado devuelto sólo contendría el campo edad
y nada más.
Todo el estado de mi reducer se vuelve undefined después de despachar
Si tu estado se convierte inesperadamente en undefined
, probablemente te estás olvidando de devolver el estado con return
en uno de los casos, o tu tipo de acción no coincide con ninguna de las declaraciones case
. Para saber por qué, lanza un error fuera del switch
:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ...
}
case 'edited_name': {
// ...
}
}
throw Error('Unknown action: ' + action.type);
}
También puedes utilizar un comprobador de tipos estático como TypeScript para detectar estos errores.
Recibo un error: «Too many re-renders»
Puede que obtengas un error que dice: Too many re-renders. React limits the number of renders to prevent an infinite loop.
(Demasiados rerenderizados. React limita el número de renderizados para evitar un bucle infinito). Normalmente, esto significa que estás enviando incondicionalmente una acción durante la renderización, por lo que tu componente entra en un bucle: renderización, envío (que provoca una renderización), renderización, envío (que provoca una renderización), y así sucesivamente. Muy a menudo, esto es causado por un error al especificar un controlador de evento:
// 🚩 Wrong: calls the handler during render
return <button onClick={handleClick()}>Hazme clic</button>
// ✅ Correct: passes down the event handler
return <button onClick={handleClick}>Hazme clic</button>
// ✅ Correct: passes down an inline function
return <button onClick={(e) => handleClick(e)}>Hazme clic</button>
Si no puedes encontrar la causa de este error, haz clic en la flecha situada junto al error en la consola y busque en la pila de JavaScript la llamada específica a la función dispatch
responsable del error.
Mi función reductora o inicializadora se ejecuta dos veces
En Modo Estricto, React llamará a tus funciones reductoras e inicializadoras dos veces. Esto no debería romper tu código.
Este comportamiento sólo para desarrollo te ayuda a mantener los componentes puros. React utiliza el resultado de una de las llamadas, e ignora el resultado de la otra llamada. Mientras tus funciones de componente, inicializadora y reducer sean puras, esto no debería afectar a tu lógica. Sin embargo, si accidentalmente son impuras, esto te ayuda a detectar los errores.
Por ejemplo, esta función reducer impura muta un array en estado:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// 🚩 Mistake: mutating state
state.todos.push({ id: nextId++, text: action.text });
return state;
}
// ...
}
}
Como React llama a tu función reductora dos veces, verás que la tarea se ha añadido dos veces, así que sabrás que hay un error. En este ejemplo, puedes corregir el error reemplazando el array en lugar de mutarlo:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// ✅ Correct: replacing with new state
return {
...state,
todos: [
...state.todos,
{ id: nextId++, text: action.text }
]
};
}
// ...
}
}
Ahora que esta función reducer es pura, llamarla una vez extra no hace ninguna diferencia en el comportamiento. Esta es la razón por la que React llamándola dos veces te ayuda a encontrar errores. Los controladores de eventos no necesitan ser puros. así que React nunca llamará a tus controladores de eventos dos veces.
Lee mantener los componentes puros para obtener más información.