useReducer no React: estado com muitas transições
Como trocar vários useState por um reducer, o formato da action e a linha em que o reducer fica melhor que o useState, com um carrinho real.
Você remove um livro do carrinho, mas o frete continua grátis. O cupom aparece na tela, porém o total volta ao preço cheio. O clique funcionou; a memória da interface terminou com informações que não combinam.
React é uma biblioteca JavaScript para criar interfaces. Um componente é uma função que descreve uma parte da tela, e cada execução dessa função para calcular a interface é um render.
Essa memória é o estado, e cada passagem dele para o próximo valor após um
evento é uma transição. useReducer é um Hook — função do React cujo
nome começa com use — que reúne essas mudanças: uma ação descreve o fato,
dispatch a envia e o reducer calcula o próximo estado. Você vai aplicar
isso à Página 7, uma livraria fictícia, reproduzindo o bug e conferindo a
correção após cada clique.
Como funciona o useReducer no React?
Ele guarda o estado e concentra no reducer as regras acionadas por dispatch.
O componente anuncia o evento; a função decide as consequências.
Pense numa central de pedidos. O registro no balcão é o estado; cada cartão com
“livro adicionado” ou “cupom aplicado” é uma ação; dispatch entrega o cartão;
e a central, como o reducer, consulta o registro atual e produz outro. O total
é consequência desse registro, não uma ordem no cartão.
A analogia termina aí: não existe uma fila física nem um banco de dados. O React agenda a atualização e chama uma função pura: entradas iguais geram o mesmo resultado, sem alterar o registro recebido nem causar efeitos externos.
Antes do código, escreva as ações em português e separe evento de resultado.
“Livro foi adicionado” é um evento; “total agora é 89,90” é consequência. Isso
evita setTotal, um cartão que manda obedecer em vez de contar o ocorrido.
Quando vários useState indicam a hora do useReducer?
O sinal é ter partes que mudam juntas para preservar uma invariante, regra que deve continuar verdadeira, como “carrinho vazio não tem cupom”.
O primeiro carrinho costuma usar um
useState para cada informação. Cada Hook devolve o valor
e um setter, a função set... que agenda sua atualização.
Experimente você mesmo
Preveja a última linha após adicionar os dois livros, aplicar o cupom e remover Grande Sertão: frete e desconto ficam corretos? Depois siga a sequência e compare sua previsão com o resumo da tela.
import { useState } from 'react';
const CATALOGO = [
{ id: 'gs', titulo: 'Grande Sertão: Veredas', preco: 74.9 },
{ id: 'vs', titulo: 'Vidas Secas', preco: 39.9 },
];
export default function CarrinhoEspalhado() {
const [itens, setItens] = useState([]);
const [cupom, setCupom] = useState(null);
const [frete, setFrete] = useState(0);
const [total, setTotal] = useState(0);
function adicionar(livro) {
const novos = [...itens, { ...livro, quantidade: 1 }];
const subtotal = novos.reduce((s, i) => s + i.preco * i.quantidade, 0);
setItens(novos);
setFrete(subtotal >= 99 ? 0 : 12.9);
setTotal(subtotal);
}
function aplicarCupom(codigo) {
setCupom(codigo);
setTotal(total * 0.9);
}
function remover(id) {
const restantes = itens.filter((i) => i.id !== id);
setItens(restantes);
setTotal(restantes.reduce((s, i) => s + i.preco * i.quantidade, 0));
}
return (
<div>
<p id="resumo">
{itens.length} itens · cupom {String(cupom)} · frete R$ {frete.toFixed(2)} · total R$ {total.toFixed(2)}
</p>
<button id="add-gs" onClick={() => adicionar(CATALOGO[0])}>+ Grande Sertão</button>
<button id="add-vs" onClick={() => adicionar(CATALOGO[1])}>+ Vidas Secas</button>
<button id="cupom" onClick={() => aplicarCupom('LEITOR10')}>Aplicar LEITOR10</button>
<button id="rm-gs" onClick={() => remover('gs')}>- Grande Sertão</button>
</div>
);
}O import traz useState, e CATALOGO fornece os livros. As quatro duplas
guardam itens, cupom, frete e total. adicionar copia o array e aciona três
setters;
aplicarCupom muda cupom e total; remover filtra o item e refaz só o total.
No return, cada botão chama um handler, função executada no clique.
A saída registra o resumo do DOM após cada um dos quatro cliques:
Na última linha, sobrou R$ 39,90: o frete deveria voltar, mas segue zerado; o
cupom aparece, mas o total está sem desconto. remover atualizou itens e total,
porém esqueceu frete e cupom. Com a regra espalhada, cada handler vira uma nova
chance de criar essa contradição.
Até aqui: o defeito não está no clique, e sim em guardar valores derivados e repetir a mesma regra em handlers separados.
Por que o reducer deve ser uma função pura?
Porque o React pode chamar o reducer para calcular uma atualização e precisa receber um resultado previsível, sem alterações escondidas. Fora do componente, essa função também pode ser executada e testada sem renderizar a interface.
Um reducer tem duas entradas — estado atual e ação — e uma saída: o próximo estado. Ele não chama Hook nem depende do componente. Ser puro significa não modificar as entradas e não fazer tarefas externas, como salvar dados ou chamar uma API, serviço com o qual o código se comunica.
export const CUPONS = { LEITOR10: 0.1, PAGINA7: 0.07 };
export const carrinhoVazio = { itens: [], cupom: null };
export function carrinhoReducer(estado, acao) {
switch (acao.tipo) {
case 'livro_adicionado': {
const jaEsta = estado.itens.some((i) => i.id === acao.livro.id);
const itens = jaEsta
? estado.itens.map((i) =>
i.id === acao.livro.id ? { ...i, quantidade: i.quantidade + 1 } : i,
)
: [...estado.itens, { ...acao.livro, quantidade: 1 }];
return { ...estado, itens };
}
case 'quantidade_aumentada':
return {
...estado,
itens: estado.itens.map((i) =>
i.id === acao.id ? { ...i, quantidade: i.quantidade + 1 } : i,
),
};
case 'livro_removido': {
const itens = estado.itens.filter((i) => i.id !== acao.id);
return { itens, cupom: itens.length === 0 ? null : estado.cupom };
}
case 'cupom_aplicado':
if (estado.itens.length === 0) return estado;
if (!CUPONS[acao.codigo]) return estado;
return { ...estado, cupom: acao.codigo };
case 'carrinho_limpo':
return carrinhoVazio;
default:
throw new Error(`Ação desconhecida no carrinho: ${acao.tipo}`);
}
}
export function resumo(estado) {
const subtotal = estado.itens.reduce((s, i) => s + i.preco * i.quantidade, 0);
const desconto = subtotal * (CUPONS[estado.cupom] ?? 0);
const frete = subtotal === 0 || subtotal - desconto >= 99 ? 0 : 12.9;
return {
subtotal: Number(subtotal.toFixed(2)),
desconto: Number(desconto.toFixed(2)),
frete,
total: Number((subtotal - desconto + frete).toFixed(2)),
};
}CUPONS reúne códigos e percentuais, enquanto carrinhoVazio define o ponto de
partida. O switch compara acao.tipo; cada
case trata um evento, e default rejeita um tipo não previsto. some procura
o livro, map cria a lista com a quantidade atualizada, filter remove uma
linha e o operador ... copia objetos e arrays — as listas do JavaScript. São
operações usadas para
atualizar estado sem mutar, isto é, sem
escrever por cima das referências recebidas.
Cada cartão aceito sai da central com um registro coerente. Ao remover o último
livro, por exemplo, o mesmo case limpa o cupom. Já resumo calcula subtotal,
desconto, frete e total a partir de itens e cupom. Eles são valores
derivados — podem ser obtidos de outros dados — e por isso não precisam ser
armazenados como quatro fontes de verdade.
Como simular uma sequência de ações no reducer?
Chame o reducer diretamente, passando o resultado de uma ação como estado da próxima. Isso expõe cada transição sem navegador, build ou componente e produz um roteiro reproduzível para investigar regras do carrinho.
import { carrinhoReducer, carrinhoVazio, resumo } from './carrinhoReducer.js';
const grandeSertao = { id: 'gs', titulo: 'Grande Sertão: Veredas', preco: 74.9 };
const vidasSecas = { id: 'vs', titulo: 'Vidas Secas', preco: 39.9 };
const roteiro = [
{ tipo: 'livro_adicionado', livro: grandeSertao },
{ tipo: 'livro_adicionado', livro: vidasSecas },
{ tipo: 'livro_adicionado', livro: vidasSecas },
{ tipo: 'cupom_aplicado', codigo: 'LEITOR10' },
{ tipo: 'livro_removido', id: 'gs' },
{ tipo: 'carrinho_limpo' },
];
let estado = carrinhoVazio;
for (const acao of roteiro) {
estado = carrinhoReducer(estado, acao);
const itens = estado.itens.map((i) => `${i.id}x${i.quantidade}`).join(' ') || '(vazio)';
const { total } = resumo(estado);
console.log(
`${acao.tipo.padEnd(20)} -> itens: ${itens.padEnd(12)} cupom: ${String(estado.cupom).padEnd(9)} total: ${total}`,
);
}Os imports trazem estado inicial, reducer e cálculo do resumo. Os dois objetos
representam o catálogo; roteiro é a fila de seis cartões. A variável estado
começa vazia, e o for...of entrega uma ação por vez, guarda o retorno como
novo estado e imprime itens, cupom e total. padEnd apenas alinha as colunas do
log; não participa da regra do carrinho.
O terceiro livro_adicionado não criou uma segunda linha de Vidas Secas: virou
vsx2. E o livro_removido derrubou o total de R$ 139,23 para R$ 84,72 —
subtotal de R$ 79,80, menos R$ 7,98 de cupom, mais os R$ 12,90 de frete que
voltaram sozinhos, porque o carrinho caiu abaixo dos R$ 99.
Esse arquivo não importa React: a regra virou uma função que você roda direto. Se uma conta quebrar, o roteiro de ações registra o caminho exato até o estado incorreto.
Até aqui: o reducer concentra transições, e uma sequência de ações comprova o comportamento antes de qualquer interface entrar em cena.
O que o dispatch deve descrever?
Ele deve enviar o fato, como livro_adicionado, sem antecipar o novo estado.
useReducer recebe reducer e estado inicial e devolve a dupla estado atual e
dispatch.
import { useReducer } from 'react';
import { carrinhoReducer, carrinhoVazio, resumo } from './carrinhoReducer.js';
const CATALOGO = [
{ id: 'gs', titulo: 'Grande Sertão: Veredas', preco: 74.9 },
{ id: 'vs', titulo: 'Vidas Secas', preco: 39.9 },
];
export default function Carrinho() {
const [estado, dispatch] = useReducer(carrinhoReducer, carrinhoVazio);
const { subtotal, desconto, frete, total } = resumo(estado);
return (
<div>
<p id="resumo">
{estado.itens.length} itens · cupom {String(estado.cupom)} · subtotal R$ {subtotal.toFixed(2)} · desconto R$ {desconto.toFixed(2)} · frete R$ {frete.toFixed(2)} · total R$ {total.toFixed(2)}
</p>
<button id="add-gs" onClick={() => dispatch({ tipo: 'livro_adicionado', livro: CATALOGO[0] })}>
+ Grande Sertão
</button>
<button id="add-vs" onClick={() => dispatch({ tipo: 'livro_adicionado', livro: CATALOGO[1] })}>
+ Vidas Secas
</button>
<button id="cupom" onClick={() => dispatch({ tipo: 'cupom_aplicado', codigo: 'LEITOR10' })}>
Aplicar LEITOR10
</button>
<button id="rm-gs" onClick={() => dispatch({ tipo: 'livro_removido', id: 'gs' })}>
- Grande Sertão
</button>
<button id="rm-vs" onClick={() => dispatch({ tipo: 'livro_removido', id: 'vs' })}>
- Vidas Secas
</button>
</div>
);
}Na desestruturação, estado é o registro e dispatch, o mensageiro.
resumo(estado) deriva os números; os botões só enviam evento e dados, como
livro, código ou id. As regras continuam no reducer.
Os mesmos cliques da primeira versão, mais um para esvaziar o carrinho:
Em #rm-gs, frete e desconto são recalculados sem ampliar o handler; em
#rm-vs, o case 'livro_removido' também limpa o cupom. Nomes como
livro_adicionado descrevem o que aconteceu, enquanto setItens apenas
repetiria um setter e esconderia as demais consequências.
Experimente você mesmo
Preveja o registro após duas adições do mesmo livro e um cupom: quantas linhas, qual quantidade e qual código? Depois dispare os três cliques e compare com a saída.
Outra função pode envolver o reducer para registrar ação e resultado:
import { useReducer } from 'react';
import { carrinhoReducer, carrinhoVazio } from './carrinhoReducer.js';
function comLog(reducer) {
return (estado, acao) => {
const proximo = reducer(estado, acao);
const itens = proximo.itens.map((i) => `${i.id} x${i.quantidade}`).join(', ');
console.log(acao.tipo.padEnd(20), '->', itens || '(vazio)', '| cupom:', proximo.cupom);
return proximo;
};
}
const carrinhoComLog = comLog(carrinhoReducer);
export default function CarrinhoComLog() {
const [estado, dispatch] = useReducer(carrinhoComLog, carrinhoVazio);
// ...os mesmos botões despachando as mesmas ações
}comLog devolve um reducer que chama a regra, imprime ação, itens e cupom e
repassa o próximo estado. É um middleware: camada que observa ou complementa
o caminho da atualização. A chamada fica fora do componente para não criar uma
função nova a cada render.
Como o reducer impede estados impossíveis?
Ele valida cada ação antes de criar o próximo estado. Uma transição inválida pode devolver o mesmo objeto, preservando a invariante e permitindo que o React ignore a atualização dos filhos.
No case 'cupom_aplicado', duas linhas devolvem estado sem mudança quando o
carrinho está vazio ou o código não existe. A central recebe o cartão, vê que a
regra não permite a ação e devolve exatamente o registro que já tinha.
import { useReducer } from 'react';
import { carrinhoReducer, carrinhoVazio, resumo } from './carrinhoReducer.js';
const GS = { id: 'gs', titulo: 'Grande Sertão: Veredas', preco: 74.9 };
export const contagem = { carrinho: 0, rodape: 0 };
function Rodape() {
contagem.rodape++;
return <small>Frete grátis acima de R$ 99</small>;
}
export default function CarrinhoContado() {
contagem.carrinho++;
const [estado, dispatch] = useReducer(carrinhoReducer, carrinhoVazio);
const { total } = resumo(estado);
return (
<div>
<p id="resumo">
{estado.itens.length} itens · cupom {String(estado.cupom)} · total R$ {total.toFixed(2)}
</p>
<button onClick={() => dispatch({ tipo: 'livro_adicionado', livro: GS })}>+ Grande Sertão</button>
<button onClick={() => dispatch({ tipo: 'cupom_aplicado', codigo: 'LEITOR10' })}>Aplicar LEITOR10</button>
<button onClick={() => dispatch({ tipo: 'cupom_aplicado', codigo: 'XPTO' })}>Aplicar XPTO</button>
<Rodape />
</div>
);
}O cupom não gruda em carrinho vazio nem com código inventado — a tela não muda.
E o Rodape não roda de novo nesses dois cliques. O React compara o estado
anterior e o próximo com Object.is, uma comparação de identidade. Ele ainda
pode chamar CarrinhoContado para conferir a atualização, mas, ao receber a
mesma referência, descarta esse resultado e pula os filhos. É a diferença entre
Carrinho rodou 4x e Rodape 2x na quarta linha. Se esse mecanismo te
interessa, ele é o assunto de
por que o componente roda de novo.
Com quatro useState soltos, nada impede um setCupom('XPTO') chamado de um
canto qualquer da tela. Com o reducer, existe um único portão por onde toda
mudança passa, e a regra “cupom só existe com carrinho cheio” mora nele.
Por que um reducer que muta não atualiza a tela?
Porque mutação altera o objeto, mas mantém sua referência — a identidade comparada pelo React. Devolver esse mesmo objeto pode fazer a atualização ser descartada.
No balcão, é como rasurar o registro antigo e devolvê-lo com a mesma etiqueta, em vez de produzir um registro novo. O reducer abaixo faz isso ao alterar o array recebido e devolver o mesmo estado:
import { useReducer } from 'react';
const GS = { id: 'gs', titulo: 'Grande Sertão: Veredas', preco: 74.9 };
function reducerQueMuta(estado, acao) {
switch (acao.tipo) {
case 'livro_adicionado':
estado.itens.push({ ...acao.livro, quantidade: 1 });
return estado;
default:
return estado;
}
}
export default function CarrinhoMutante() {
const [estado, dispatch] = useReducer(reducerQueMuta, { itens: [] });
console.log('render — itens no estado:', estado.itens.length);
return (
<div>
<p id="resumo">Carrinho: {estado.itens.length} livros</p>
<button onClick={() => dispatch({ tipo: 'livro_adicionado', livro: GS })}>
+ Grande Sertão
</button>
</div>
);
}push escreve no array original, e o reducer retorna estado. O componente
conta renders e mostra o tamanho; por compartilhar a referência antiga, log e
DOM passam a discordar:
O log vê 1 e depois 2 itens, mas a tela continua em zero. Como o objeto devolvido é o mesmo, o React descarta o resultado que o componente calculou.
O que acontece quando o tipo da action está errado?
O default lança uma exceção no ponto em que a ação desconhecida chega. Em vez
de deixar um estado inválido avançar, a mensagem mostra qual tipo precisa ser
corrigido.
Aqui, o reducer espera livro_adicionado, mas recebe adicionar_livro:
import { carrinhoReducer, carrinhoVazio } from './carrinhoReducer.js';
const GS = { id: 'gs', titulo: 'Grande Sertão: Veredas', preco: 74.9 };
carrinhoReducer(carrinhoVazio, { tipo: 'adicionar_livro', livro: GS });Error: Ação desconhecida no carrinho: adicionar_livro at carrinhoReducer (file:///private/tmp/pagina7/carrinhoReducer.js:39:13) at file:///private/tmp/pagina7/carrinho.mjs:5:1 at ModuleJob.run (node:internal/modules/esm/module_job:439:25) at async node:internal/modules/esm/loader:633:26 at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) Node.js v24.16.0
A mensagem traz o nome errado. Aqui o reducer foi chamado no Node para deixar o rastro limpo; no navegador, a exceção chega ao error boundary, componente preparado para capturar falhas de renderização, com a mesma frase.
Sem o default que lança, esse reducer devolveria undefined como novo
estado, e a quebra apareceria só no render seguinte, longe da causa, com um
Cannot read properties of undefined. O throw no default troca um bug
silencioso por um erro no lugar certo. Com TypeScript, os tipos aceitos podem
virar uma união, isto é, uma lista fechada que o editor verifica antes da
execução.
Até aqui: retornar uma referência antiga pode esconder a atualização; rejeitar um tipo desconhecido torna o outro erro visível perto da causa.
Como combinar useReducer com Context API?
Context disponibiliza estado e dispatch a componentes distantes sem repassar
essas props por cada nível. Dois contextos deixam cada componente ler só o que
usa.
Um carrinho aparece no cabeçalho, na estante e no fechamento. Props são dados
passados de pai para filho; atravessar níveis que não as usam é prop drilling.
A
Context API cria um canal compartilhado: o
provider oferece o valor, e componentes consumidores o leem com
useContext.
O detalhe que faz esse par funcionar é a estabilidade do dispatch:
import { useReducer } from 'react';
import { carrinhoReducer, carrinhoVazio } from './carrinhoReducer.js';
const GS = { id: 'gs', titulo: 'Grande Sertão: Veredas', preco: 74.9 };
let anterior = null;
export default function DispatchEstavel() {
const [estado, dispatch] = useReducer(carrinhoReducer, carrinhoVazio);
console.log(
'render',
estado.itens.length,
'— mesmo dispatch de antes?',
anterior === null ? '(primeiro render)' : Object.is(anterior, dispatch),
);
anterior = dispatch;
return <button onClick={() => dispatch({ tipo: 'livro_adicionado', livro: GS })}>+ Grande Sertão</button>;
}O React garante identidade estável para dispatch, confirmada por Object.is
na saída. Como DispatchContexto recebe exatamente essa função, mudar o
carrinho não altera esse contexto nem notifica consumidores por esse motivo.
Eles ainda podem renderizar por causa do pai, do próprio estado ou de outro
contexto.
import { createContext, useContext, useReducer } from 'react';
import { carrinhoReducer, carrinhoVazio, resumo } from './carrinhoReducer.js';
const CarrinhoContexto = createContext(null);
const DispatchContexto = createContext(null);
export function CarrinhoProvider({ children }) {
const [estado, dispatch] = useReducer(carrinhoReducer, carrinhoVazio);
return (
<CarrinhoContexto.Provider value={estado}>
<DispatchContexto.Provider value={dispatch}>{children}</DispatchContexto.Provider>
</CarrinhoContexto.Provider>
);
}
export const useCarrinho = () => useContext(CarrinhoContexto);
export const useDispatchCarrinho = () => useContext(DispatchContexto);
function Cabecalho() {
const estado = useCarrinho();
const { total } = resumo(estado);
return <header id="cabecalho">Página 7 · {estado.itens.length} livros · R$ {total.toFixed(2)}</header>;
}
function CartaoLivro({ livro }) {
const dispatch = useDispatchCarrinho();
return (
<button id={`add-${livro.id}`} onClick={() => dispatch({ tipo: 'livro_adicionado', livro })}>
{livro.titulo}
</button>
);
}createContext abre os canais; CarrinhoProvider publica estado e dispatch;
children é a árvore dentro dele. Os Hooks próprios escondem useContext.
Cabecalho lê estado, enquanto CartaoLivro recebe o livro e lê dispatch.
O CartaoLivro está três níveis abaixo do provider e não recebeu nenhuma prop
de carrinho. Ele conhece um livro e sabe anunciar um fato. Envolver o
useContext em hooks customizados como
useCarrinho também deixa um lugar para lançar um erro claro se alguém usar o
hook fora do provider.
Como escrever testes automatizados para o reducer?
Passe estados e ações conhecidos ao reducer e compare o retorno com o resultado esperado. Como ele é puro, o teste não precisa montar componente nem simular o DOM.
node:test é o executor de testes incluído no Node; assert oferece as
verificações que fazem o teste falhar quando um valor foge do esperado.
import test from 'node:test';
import assert from 'node:assert/strict';
import { carrinhoReducer, carrinhoVazio, resumo } from './carrinhoReducer.js';
const GS = { id: 'gs', titulo: 'Grande Sertão: Veredas', preco: 74.9 };
test('o mesmo livro duas vezes vira quantidade 2, não duas linhas', () => {
let estado = carrinhoReducer(carrinhoVazio, { tipo: 'livro_adicionado', livro: GS });
estado = carrinhoReducer(estado, { tipo: 'livro_adicionado', livro: GS });
assert.equal(estado.itens.length, 1);
assert.equal(estado.itens[0].quantidade, 2);
});
test('remover o último livro derruba o cupom junto', () => {
let estado = carrinhoReducer(carrinhoVazio, { tipo: 'livro_adicionado', livro: GS });
estado = carrinhoReducer(estado, { tipo: 'cupom_aplicado', codigo: 'LEITOR10' });
estado = carrinhoReducer(estado, { tipo: 'livro_removido', id: 'gs' });
assert.deepEqual(estado, carrinhoVazio);
assert.equal(resumo(estado).total, 0);
});
test('cupom em carrinho vazio devolve o mesmo objeto', () => {
const depois = carrinhoReducer(carrinhoVazio, { tipo: 'cupom_aplicado', codigo: 'LEITOR10' });
assert.ok(Object.is(carrinhoVazio, depois));
});
test('o reducer não encosta no estado que recebeu', () => {
const antes = { itens: [{ ...GS, quantidade: 1 }], cupom: null };
const copia = structuredClone(antes);
carrinhoReducer(antes, { tipo: 'quantidade_aumentada', id: 'gs' });
assert.deepEqual(antes, copia);
});Os dois primeiros testes repetem livro e removem o último item para verificar
quantidade, cupom e total. O terceiro usa Object.is para confirmar que uma
ação inválida devolve exatamente o mesmo estado. O quarto cria uma cópia com
structuredClone, executa uma ação e compara o estado original com essa cópia;
se o reducer o tivesse mutado, deepEqual encontraria a diferença.
node --testO comando node --test encontrou quatro casos, e pass 4 com fail 0 confirma
que todos passaram nessa execução. A duração mede a corrida; não é uma promessa
de desempenho para outras máquinas.
Para que serve o terceiro argumento do useReducer?
Ele recebe uma função inicializadora que transforma o segundo argumento no estado da montagem. Isso evita repetir, a cada render, uma preparação que só é necessária no início.
Um uso comum é a reidratação: reconstruir o estado a partir de texto salvo
no localStorage, o armazenamento do navegador. JSON.parse transforma esse
texto em formato JSON — uma forma padronizada de representar dados — em valores
JavaScript, que ainda precisam ser validados porque podem estar antigos ou
corrompidos.
import { useReducer } from 'react';
import { carrinhoReducer, carrinhoVazio, resumo } from './carrinhoReducer.js';
// o rascunho que chega do localStorage, com uma linha corrompida no meio:
// {"itens":[{"id":"vs","titulo":"Vidas Secas","preco":39.9,"quantidade":2},
// {"id":"","quantidade":0}],"cupom":"LEITOR10"}
function reidratar(bruto) {
console.log('reidratar rodou');
try {
const salvo = JSON.parse(bruto);
const itens = (salvo.itens ?? []).filter((i) => i.id && i.quantidade > 0);
return { itens, cupom: itens.length > 0 ? (salvo.cupom ?? null) : null };
} catch {
return carrinhoVazio;
}
}
export default function CarrinhoSalvo({ rascunho }) {
const [estado, dispatch] = useReducer(carrinhoReducer, rascunho, reidratar);
const { total } = resumo(estado);
// ...
}reidratar converte o texto, filtra linhas sem id ou quantidade positiva e só
conserva o cupom se houver itens; catch cobre JSON inválido. Em useReducer,
rascunho é a entrada bruta e reidratar, o terceiro argumento.
Uma linha era válida e outra corrompida, por isso a montagem mostra um item. Nesta medição sem StrictMode, “reidratar rodou” apareceu uma vez. Em StrictMode de desenvolvimento, o React pode chamar a inicializadora duas vezes e usar só um resultado; ela também precisa ser pura. Sem o terceiro argumento, parse e filtro no corpo se repetiriam a cada render.
Quando usar useReducer ou useState?
Use useState para valores independentes e transições curtas; considere
useReducer quando várias mudanças dependem umas das outras, regras se repetem
ou você precisa testar a lógica separadamente. A tabela transforma esses sinais
em uma decisão prática.
| situação | com useState | com useReducer |
|---|---|---|
| um valor solto: o texto de um input, um modal aberto | é o caminho natural | cerimônia sem retorno |
| dois ou três valores que mudam em momentos diferentes | funciona bem | desnecessário |
| quatro ou mais campos que mudam sempre juntos | quatro setters em cada handler | uma action por handler |
| a próxima mudança depende de outra parte do estado | você lê valor velho ou empilha useEffect |
o reducer recebe o estado inteiro |
| a mesma mudança é disparada de três telas | o handler é copiado três vezes | um case, três dispatch |
| quer testar a regra sem montar componente | precisa renderizar | é uma função exportada |
| existe combinação de valores que não pode existir | nada impede | o reducer é o único portão |
A pergunta prática é: alguém consegue listar tudo que pode acontecer com esse
estado olhando um único lugar? Com o reducer, a resposta está no switch.
Com setters espalhados, é preciso rastrear todos os handlers.
E não é uma escolha para o componente inteiro: o carrinho pode viver num
reducer, e o campo de busca do cabeçalho, ao lado, continuar num useState de
uma linha. Migrar depois também é barato — os quatro useState da primeira
seção viraram este reducer com os mesmos botões, na mesma ordem, trocando três
chamadas de setter por um dispatch.
Até aqui: escolha o Hook pela forma das transições, não pela quantidade de
linhas. Um valor isolado pode continuar em useState ao lado de um carrinho
governado por reducer.
O que aprender depois de useReducer?
O próximo passo da trilha é entender como children — o conteúdo colocado
dentro de um componente — e composição organizam a árvore em que esse estado
circula. Você vai separar responsabilidades sem criar um componente gigante nem
repassar detalhes desnecessários.
Continue em children e composição no React. Depois, a trilha de React leva a rotas e organização do projeto.
Perguntas frequentes
useReducer substitui o Redux?
Posso chamar uma API dentro do reducer?
Preciso usar switch no reducer?
O dispatch precisa entrar na lista de dependências do useEffect?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- React — useReducer — react.dev
- React — Extracting State Logic into a Reducer — react.dev
- React — Scaling Up with Reducer and Context — react.dev


