useReducer
useReducer
é um Hook do React que permite adicionar um reducer ao seu componente.
const [state, dispatch] = useReducer(reducer, initialArg, init?)
- Referência
- Uso
- Solução de Problemas
- Eu dispatch uma ação, mas o log me dá o antigo valor do estado
- Eu dispatch uma ação, mas a tela não atualiza
- Uma parte do meu estado do reducer se torna indefinida após a dispatch
- Meu estado inteiro do reducer se torna indefinido após a dispatch
- Estou recebendo um erro: “Muitas re-renderizações”
- Meu reducer ou função inicializadora é chamado duas vezes
Referência
useReducer(reducer, initialArg, init?)
Chame useReducer
na raiz do seu componente para gerenciar seu estado com um reducer.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
Parâmetros
reducer
: A função reducer que especifica como o estado deve ser atualizado. Deve ser pura, deve receber o estado e a ação como argumentos, e deve retornar o próximo estado. Estado e ação podem ser de quaisquer tipos.initialArg
: O valor a partir do qual o estado inicial é calculado. Pode ser um valor de qualquer tipo. Como o estado inicial é calculado a partir dele depende do próximo argumentoinit
.- opcional
init
: A função de inicialização que deve retornar o estado inicial. Se não for especificada, o estado inicial é definido comoinitialArg
. Caso contrário, o estado inicial é definido como o resultado da chamadainit(initialArg)
.
Retornos
useReducer
retorna um array com exatamente dois valores:
- O estado atual. Durante a primeira renderização, é definido como
init(initialArg)
ouinitialArg
(se não houverinit
). - A
função dispatch
que permite atualizar o estado para um valor diferente e acionar uma nova renderização.
Ressalvas
useReducer
é um Hook, então você só pode chamá-lo na raiz do seu componente ou em seus próprios Hooks. Você não pode chamá-lo dentro de loops ou condições. Se precisar disso, extraia um novo componente e mova o estado para ele.- No Modo Estrito, o React chamará seu reducer e inicializador duas vezes para ajudar você a encontrar impurezas acidentais. Este comportamento é exclusivo do desenvolvimento e não afeta a produção. Se seu reducer e inicializador forem puros (como deveriam ser), isso não deve afetar sua lógica. O resultado de uma das chamadas é ignorado.
Função dispatch
A função dispatch
retornada pelo useReducer
permite atualizar o estado para um valor diferente e acionar uma nova renderização. Você precisa passar a ação como o único argumento para a função dispatch
:
const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
// ...
O React definirá o próximo estado como o resultado da chamada da função reducer
que você forneceu com o state
atual e a ação que você passou ao dispatch
.
Parâmetros
action
: A ação realizada pelo usuário. Pode ser um valor de qualquer tipo. Por convenção, uma ação é geralmente um objeto com uma propriedadetype
identificando-a e, opcionalmente, outras propriedades com informações adicionais.
Retornos
As funções dispatch
não têm um valor de retorno.
Ressalvas
-
A função
dispatch
apenas atualiza a variável de estado para o próximo render. Se você ler a variável de estado após chamar a funçãodispatch
, ainda obterá o antigo valor que estava na tela antes da sua chamada. -
Se o novo valor que você fornecer for idêntico ao
state
atual, conforme determinado por uma comparaçãoObject.is
, o React pulará a renderização do componente e de seus filhos. Esta é uma otimização. O React ainda pode precisar chamar seu componente antes de ignorar o resultado, mas isso não deve afetar seu código. -
O React agrupa atualizações de estado. Ele atualiza a tela após todos os manipuladores de eventos terem sido executados e chamaram suas funções
set
. Isso previne várias re-renderizações durante um único evento. No raro caso de você precisar forçar o React a atualizar a tela mais cedo, por exemplo, para acessar o DOM, você pode usarflushSync
.
Uso
Adicionando um reducer a um componente
Chame useReducer
na raiz do seu componente para gerenciar o estado com um reducer.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
useReducer
retorna um array com exatamente dois itens:
- O estado atual desta variável de estado, inicialmente definido como o estado inicial que você forneceu.
- A função
dispatch
que permite você mudá-lo em resposta à interação.
Para atualizar o que está na tela, chame dispatch
com um objeto representando o que o usuário fez, chamado de ação:
function handleClick() {
dispatch({ type: 'incremented_age' });
}
O React passará o estado atual e a ação para sua função reducer. Seu reducer calculará e retornará o próximo estado. O React armazenará esse próximo estado, renderizará seu componente com ele e atualizará a UI.
import { useReducer } from 'react'; function reducer(state, action) { if (action.type === 'incremented_age') { return { age: state.age + 1 }; } throw Error('Ação desconhecida.'); } export default function Counter() { const [state, dispatch] = useReducer(reducer, { age: 42 }); return ( <> <button onClick={() => { dispatch({ type: 'incremented_age' }) }}> Incrementar idade </button> <p>Olá! Você tem {state.age} anos.</p> </> ); }
useReducer
é muito similar ao useState
, mas permite que você mova a lógica de atualização de estado dos manipuladores de eventos para uma única função fora do seu componente. Leia mais sobre escolhendo entre useState
e useReducer
.
Escrevendo a função reducer
Uma função reducer é declarada assim:
function reducer(state, action) {
// ...
}
Então você precisa preencher o código que calculará e retornará o próximo estado. Por convenção, é comum escrevê-la como uma instrução switch
. Para cada case
no switch
, calcule e retorne algum próximo estado.
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('Ação desconhecida: ' + action.type);
}
As ações podem ter qualquer formato. Por convenção, é comum passar objetos com uma propriedade type
identificando a ação. Deve incluir as informações mínimas necessárias que o reducer precisa para calcular o próximo 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
});
}
// ...
Os nomes dos tipos de ação são locais ao seu componente. Cada ação descreve uma única interação, mesmo que isso leve a várias mudanças nos dados. A forma do estado é arbitrária, mas geralmente será um objeto ou um array.
Leia extraindo a lógica de estado em um reducer para saber mais.
Example 1 of 3: Formulário (objeto)
Neste exemplo, o reducer gerencia um objeto de estado com dois campos: name
e 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('Ação desconhecida: ' + 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 idade </button> <p>Olá, {state.name}. Você tem {state.age} anos.</p> </> ); }
Evitando recriações do estado inicial
O React salva o estado inicial uma vez e o ignora nas renderizações seguintes.
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
// ...
Embora o resultado de createInitialState(username)
seja usado apenas para a renderização inicial, você ainda está chamando essa função em todas as renderizações. Isso pode ser desperdício se estiver criando arrays grandes ou realizando cálculos custosos.
Para resolver isso, você pode passá-la como uma função inicializadora para useReducer
como o terceiro argumento:
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...
Observe que você está passando createInitialState
, que é a função em si, e não createInitialState()
, que é o resultado de sua chamada. Dessa forma, o estado inicial não é recriado após a inicialização.
No exemplo acima, createInitialState
recebe um argumento username
. Se seu inicializador não precisar de informações para calcular o estado inicial, você pode passar null
como o segundo argumento para useReducer
.
Example 1 of 2: Passando a função inicializadora
Este exemplo passa a função inicializadora, então a função createInitialState
é executada apenas durante a inicialização. Ela não é executada quando o componente é renderizado novamente, como ao digitar no input.
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('Ação desconhecida: ' + 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' }); }}>Adicionar</button> <ul> {state.todos.map(item => ( <li key={item.id}> {item.text} </li> ))} </ul> </> ); }
Solução de Problemas
Eu dispatch uma ação, mas o log me dá o antigo valor do estado
Chamar a função dispatch
não altera o estado no código em execução:
function handleClick() {
console.log(state.age); // 42
dispatch({ type: 'incremented_age' }); // Solicita uma nova renderização com 43
console.log(state.age); // Ainda 42!
setTimeout(() => {
console.log(state.age); // Também 42!
}, 5000);
}
Isso acontece porque os estados se comportam como um instantâneo. Atualizar o estado solicita uma nova renderização com o novo valor de estado, mas não afeta a variável de state
em seu manipulador de eventos que já está em execução.
Se você precisar adivinhar o próximo valor de estado, pode calculá-lo manualmente chamando o reducer você mesmo:
const action = { type: 'incremented_age' };
dispatch(action);
const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }
Eu dispatch uma ação, mas a tela não atualiza
O React ignora sua atualização se o próximo estado for igual ao estado anterior, conforme determinado por uma comparação Object.is
. Isso geralmente acontece quando você modifica um objeto ou um array diretamente no estado:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Errado: mutando o objeto existente
state.age++;
return state;
}
case 'changed_name': {
// 🚩 Errado: mutando o objeto existente
state.name = action.nextName;
return state;
}
// ...
}
}
Você mutou um objeto state
existente e o retornou, então o React ignorou a atualização. Para corrigir isso, você precisa garantir que está sempre atualizando objetos no estado e atualizando arrays no estado em vez de mutá-los:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Correto: criando um novo objeto
return {
...state,
age: state.age + 1
};
}
case 'changed_name': {
// ✅ Correto: criando um novo objeto
return {
...state,
name: action.nextName
};
}
// ...
}
}
Uma parte do meu estado do reducer se torna indefinida após a dispatch
Certifique-se de que cada branch de case
copia todos os campos existentes ao retornar o novo estado:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state, // Não esqueça disso!
age: state.age + 1
};
}
// ...
Sem o ...state
acima, o próximo estado retornado conteria apenas o campo age
e mais nada.
Meu estado inteiro do reducer se torna indefinido após a dispatch
Se o seu estado inesperadamente se torna undefined
, você provavelmente está esquecendo de return
estado em um dos cases, ou seu tipo de ação não corresponde a nenhuma das instruções case
. Para descobrir o motivo, lance um erro fora do switch
:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ...
}
case 'edited_name': {
// ...
}
}
throw Error('Ação desconhecida: ' + action.type);
}
Você também pode usar um verificador de tipo estático como TypeScript para pegar esses erros.
Estou recebendo um erro: “Muitas re-renderizações”
Você pode receber um erro que diz: Muitas re-renderizações. O React limita o número de renderizações para evitar um loop infinito.
Normalmente, isso significa que você está dispatchando uma ação incondicionalmente durante a renderização, então seu componente entra em um loop: renderizar, dispatch (o que causa uma renderização), renderizar, dispatch (o que causa uma renderização), e assim por diante. Muito frequentemente, isso é causado por um erro ao especificar um manipulador de eventos:
// 🚩 Errado: chama o manipulador durante a renderização
return <button onClick={handleClick()}>Clique em mim</button>
// ✅ Correto: passa o manipulador de eventos
return <button onClick={handleClick}>Clique em mim</button>
// ✅ Correto: passa uma função inline
return <button onClick={(e) => handleClick(e)}>Clique em mim</button>
Se você não conseguir encontrar a causa desse erro, clique na seta ao lado do erro no console e olhe através da pilha JavaScript para encontrar a chamada específica da função dispatch
responsável pelo erro.
Meu reducer ou função inicializadora é chamado duas vezes
No Modo Estrito, o React chamará suas funções reducer e inicializadora duas vezes. Isso não deve quebrar seu código.
Este comportamento é exclusivo do desenvolvimento e ajuda você a manter os componentes puros. O React usa o resultado de uma das chamadas e ignora o resultado da outra chamada. Contanto que seu componente, inicializador e funções reducer sejam puros, isso não deverá afetar sua lógica. No entanto, se forem acidentalmente impuros, isso ajudará você a notar os erros.
Por exemplo, essa função reducer impura muta um array no estado:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// 🚩 Erro: mutando o estado
state.todos.push({ id: nextId++, text: action.text });
return state;
}
// ...
}
}
Como o React chama sua função reducer duas vezes, você verá que a tarefa foi adicionada duas vezes, então saberá que há um erro. Neste exemplo, você pode corrigir o erro substituindo o array em vez de mutá-lo:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// ✅ Correto: substituindo pelo novo estado
return {
...state,
todos: [
...state.todos,
{ id: nextId++, text: action.text }
]
};
}
// ...
}
}
Agora que essa função reducer é pura, chamar ela uma vez a mais não faz diferença no comportamento. É por isso que o React chamá-la duas vezes ajuda você a encontrar erros. Somente funções de componente, inicializadora e reducer precisam ser puras. Manipuladores de eventos não precisam ser puros, então o React nunca chamará seus manipuladores de eventos duas vezes.
Leia mantendo componentes puros para saber mais.