useReducer é um hook do React que guarda estado como o useState, mas leva todas as formas de mudá-lo para uma única função, o reducer. Seu componente chama dispatch com um objeto de ação como { type: 'increment' }, e o React passa o estado atual e essa ação para o reducer, que retorna o próximo estado.
Os botões não dizem como a contagem muda, só o que aconteceu. Toda a aritmética fica em reducer. Adicione um case 'double' que retorne { count: state.count * 2 } e um botão que o despache para ver como um novo tipo de atualização se encaixa.
A sintaxe
const [state, dispatch] = useReducer(reducer, initialArg, init); // init is optional
function reducer(state, action) {
// return the next state
}
reduceré uma função(state, action) => nextState. Defina-a fora do componente; ela não precisa de nada do escopo do componente.initialArgé o estado inicial, usado só na primeira renderização.inité opcional. Se você o passar, o estado inicial passa a serinit(initialArg).stateé o estado atual para esta renderização.dispatch(action)envia uma ação para o reducer e agenda uma renderização com o resultado. Ele é estável: é a mesma função em toda renderização, então é seguro passá-lo para baixo ou listá-lo como dependência.
Uma ação pode ser qualquer valor, mas por convenção é um objeto com uma string type descrevendo o que aconteceu, mais os dados de que o reducer precisar: { type: 'added', text: 'Buy milk' }.
Escrevendo um reducer
A maioria dos reducers é um switch em action.type, com um case por tipo de evento. Cada case retorna um estado totalmente novo; nunca altera o antigo. O case default lança um erro, então um erro de digitação como dispatch({ type: 'incremnet' }) falha de forma visível em vez de não fazer nada.
Dê às ações nomes do que o usuário fez (added, toggled, deleted), não da mudança de estado que você tem em mente (setTodos). Assim o componente se lê como uma lista de eventos, e o reducer é o único lugar que decide o que cada evento significa.
Reducers precisam ser puros
Um reducer é uma função pura: dados o mesmo estado e a mesma ação, ele retorna o mesmo resultado e não faz mais nada. Isso significa nada de mutação, requisições, timers, Math.random() ou Date.now() dentro dele. O React depende disso. Em desenvolvimento com StrictMode, o React chama o seu reducer duas vezes para cada ação e fica com um resultado, para ajudar você a perceber um reducer que não é puro. A prévia aqui roda como um build de produção, então os exemplos desta página o chamam uma vez.
A regra da mutação é a que as pessoas mais quebram. Fazer push em state.todos e retornar state retorna o mesmo objeto, então o React não vê mudança e pula a renderização, a mesma armadilha descrita em atualizar arrays e objetos. Monte novos arrays e objetos com spread, map e 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] };
Efeitos colaterais que pertencem a um evento, como salvar em um servidor, vão no event handler ao lado do dispatch. Efeitos colaterais que pertencem ao estado, como sincronizá-lo com o localStorage, vão em um efeito.
Uma lista de tarefas com useReducer
Aqui o reducer faz um trabalho de verdade: adicionar, marcar e excluir tarefas. O console.log no topo do reducer imprime cada ação, um jeito prático de acompanhar como o estado muda enquanto você depura. É o único efeito colateral comumente tolerado em um reducer, porque não muda nada; tire-o antes de publicar.
Adicione uma tarefa, marque uma caixa e exclua uma, depois leia o console: cada mudança é uma linha com o nome da ação e os dados dela. O texto do input fica no useState, porque é local ao campo e nenhum outro evento mexe nele. Misturar os dois hooks em um componente é normal.
O id da nova tarefa é criado no handler de clique e viaja na ação, então o reducer só o copia. Escrever id: nextId++ dentro do reducer o tornaria impuro: com StrictMode em desenvolvimento, a segunda chamada pularia um id.
O dispatch não muda o estado na hora
dispatch funciona como um setter do useState: ele agenda uma renderização, e a variável state no seu handler atual mantém o valor antigo. Para usar o novo estado na hora, calcule-o você mesmo chamando o reducer.
Clique algumas vezes. A primeira linha do log está sempre um passo atrás, e a segunda bate com o número do botão depois da renderização. Como o reducer é uma função comum, chamá-lo você mesmo é seguro, o que é mais uma vantagem de mantê-lo puro.
useState vs useReducer
Os dois hooks guardam estado, e tudo o que você escreve com um dá para escrever com o outro. A diferença é onde a lógica de atualização mora.
| useState | useReducer | |
|---|---|---|
| Lógica de atualização | Em cada event handler | Em uma única função reducer |
| Bom para | Alguns valores independentes | Vários campos mudados juntos por muitos eventos |
| Tamanho do código | Menor para estado simples | Maior no início, menor conforme os eventos crescem |
| Testes | Teste pelo componente | Teste o reducer como uma função comum |
| Depuração | Descobrir qual handler o definiu | Registrar cada ação em um único lugar |
Recorra ao useReducer quando perceber o mesmo estado sendo atualizado em muitos handlers, quando um evento precisa mudar várias partes de estado que devem ficar consistentes ou quando um handler é quase todo lógica sobre o próximo estado. Para um toggle ou um campo de texto, o useState é mais curto e mais claro. Trocar depois não é difícil: substitua os setters por dispatches e leve a lógica para os cases.
Inicialização preguiçosa
Se você escreve o estado inicial como uma chamada de função, como useReducer(reducer, createInitialState('Ada')), essa chamada executa em toda renderização, embora o React só use o resultado na primeira vez. Se montar o estado inicial é caro, ou deve ser derivado de uma prop, passe um terceiro argumento: uma função init. O React chama init(initialArg) uma vez, na primeira renderização.
Digite no input: o componente renderiza a cada tecla, mas createInitialState registra só uma vez. Agora troque a chamada por useReducer(reducer, createInitialState('Ada')) e digite de novo. A função executa em toda renderização, e o React descarta o resultado a cada vez depois da primeira.
Passe a própria função, não o resultado de chamá-la. useReducer(reducer, 'Ada', createInitialState) é preguiçoso; useReducer(reducer, createInitialState('Ada')) não é.
useReducer com context
Um reducer combina bem com context quando uma árvore profunda precisa do estado. Coloque o estado e o dispatch em um context no topo, e qualquer componente abaixo pode ler o estado ou enviar ações sem props passando por todas as camadas. Como o dispatch nunca muda, componentes que só enviam ações podem ler um context de dispatch separado e deixar de renderizar quando o estado muda.
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>;
}
No React 19, um objeto de context funciona como o próprio provider (<TodosContext value={...}>); <TodosContext.Provider> também continua funcionando. A página do useContext explica como o context chega aos componentes e quando os faz renderizar de novo.
Perguntas frequentes
O que é useReducer no React?
Um hook para estado cujas atualizações são descritas como ações. Você chama const [state, dispatch] = useReducer(reducer, initialState) e depois dispatch({ type: 'added' }) nos event handlers. O React passa o estado atual e a ação para o seu reducer, e o que ele retornar vira o próximo estado.
Quando devo usar useReducer em vez de useState?
Quando vários eventos atualizam o mesmo estado de formas relacionadas, quando o próximo estado depende de vários campos ao mesmo tempo ou quando a lógica de atualização é longa o bastante para você querê-la em uma única função testável fora do componente. Para um ou dois valores independentes, o useState é mais simples.
Por que um reducer precisa ser puro?
O React pode chamar o seu reducer mais de uma vez para a mesma ação (o StrictMode faz isso de propósito em desenvolvimento) e espera o mesmo resultado a cada vez. Então um reducer não deve mutar o estado, buscar dados, criar timers nem ler valores aleatórios. Faça essas coisas em event handlers ou efeitos.
O dispatch atualiza o estado na hora?
Não. Como um setter do useState, o dispatch agenda uma renderização. Dentro do event handler atual, state ainda tem o valor antigo; o novo estado aparece na próxima renderização.
Qual é o terceiro argumento do useReducer?
Uma função init opcional. Quando você a passa, o React calcula o estado inicial como init(initialArg), e só na primeira renderização. É útil quando montar o estado inicial é caro ou depende de uma prop.