Menu

Hook useReducer no React: sintaxe, exemplos, vs useState

useReducer guarda estado e leva toda atualização para uma única função reducer: você despacha uma ação, e o reducer retorna o próximo estado. Aprenda a sintaxe, como escrever um reducer puro, um exemplo de lista de tarefas, a inicialização preguiçosa e quando escolhê-lo em vez de useState.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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 ser init(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.

useStateuseReducer
Lógica de atualizaçãoEm cada event handlerEm uma única função reducer
Bom paraAlguns valores independentesVários campos mudados juntos por muitos eventos
Tamanho do códigoMenor para estado simplesMaior no início, menor conforme os eventos crescem
TestesTeste pelo componenteTeste o reducer como uma função comum
DepuraçãoDescobrir qual handler o definiuRegistrar 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.

Ilustração das linguagens de programação do Coddy

Aprenda a programar com o Coddy

COMEÇAR