useState no React: guardar e atualizar o estado da tela
O que o useState devolve, por que a tela não muda quando você altera a variável direto e como usar a forma de função para não perder atualização.
Você clica em “Adicionar”, o console confirma que a variável mudou, mas a tela continua no zero. Depois chama a atualização três vezes e o número sobe apenas uma. Esses comportamentos deixam de parecer aleatórios quando você entende o que o React guarda e em que momento ele redesenha a interface.
Aqui a gente parte de um contador mínimo e chega à comanda completa de uma padaria, com objetos, total calculado e erros reais. Cada bloco foi executado de verdade; a saída abaixo dele é o resultado que apareceu no terminal.
O que são estado, hook e render no React?
O React é uma biblioteca que executa componentes para descrever a interface e atualiza a tela quando os dados mudam. Cada execução é um render; o estado é a memória que sobrevive entre renders e participa da página.
useState é um hook, uma função especial do React que conecta essa memória
a um componente de função. Ele devolve o valor atual e um setter, a função
que solicita a atualização. Hooks começam com use e ficam no topo do
componente, fora de condições e laços.
Na padaria, o estado é a comanda; o render é a atendente lendo o registro para montar o pedido; o setter entrega uma comanda atualizada; e a tela é o quadro que todos consultam. O limite da analogia é importante: o setter não apaga o número que já está sendo lido. Ele agenda outro render com o novo valor.
O que o useState devolve?
Ele devolve um array de dois itens: o valor atual e o setter. Um array é uma lista ordenada, por isso cada item também pode ser acessado pela posição. O primeiro exemplo mostra o par sem atalhos:
import { useState } from 'react';
export default function Par() {
const estado = useState(0);
console.log(estado);
console.log('valor :', estado[0]);
console.log('atualizador:', typeof estado[1]);
return <p>Pães de queijo: {estado[0]}</p>;
}O import traz o hook do React e useState(0) cria o par com valor inicial
zero. Na posição 0 está o valor; na posição 1, a função atualizadora. A
desestruturação de array lê os
dois itens e dá nome a cada um. A convenção é x e setX:
import { useState } from 'react';
export default function Balcao() {
const [paes, setPaes] = useState(0);
return (
<div>
<p>Pães de queijo: {paes}</p>
<button onClick={() => setPaes(paes + 1)}>Adicionar</button>
</div>
);
}Montando e clicando duas vezes no botão, o HTML que o React deixou na página em cada etapa foi:
Na linha principal, paes é o valor deste render, setPaes é o setter e 0 é
o valor inicial. O parágrafo mostra paes; onClick recebe a função que pede o
próximo valor. Por isso cada clique produz outro número na saída.
O valor inicial é usado só quando o componente aparece pela primeira vez. Nos renders seguintes, o React ignora esse argumento e devolve o valor guardado.
Experimente você mesmo
No segundo exemplo, troque useState(0) por useState(5). Antes de executar,
preveja os quatro números que aparecerão no HTML — o da montagem e os três
produzidos pelos cliques — e só depois confira no navegador.
Por que uma variável comum não atualiza a tela?
Porque mudar uma variável local não avisa o React e não pede outro render. Além disso, a variável seria criada de novo se o componente executasse novamente. A saída mostra os dois problemas:
export default function BalcaoQuebrado() {
let paes = 0;
function adicionar() {
paes = paes + 1;
console.log('a variável agora vale', paes);
}
console.log('render com paes =', paes);
return (
<div>
<p>Pães de queijo: {paes}</p>
<button onClick={adicionar}>Adicionar</button>
</div>
);
}Duas coisas quebraram. onClick={adicionar} entrega a função ao botão, e ela
realmente soma a variável em cada clique. Porém, “render com paes = 0” aparece
só no começo: mudar paes não pediu nova tela. Mesmo que pedisse, let paes = 0
executaria outra vez e zeraria o valor. Uma variável local nasce e morre dentro
da chamada da função.
O useState resolve os dois problemas ao mesmo tempo. Ele guarda o valor num
lugar que sobrevive entre renders, e o atualizador avisa o React de que é hora
de rodar o componente de novo.
Por que o setter não muda o estado na mesma hora?
Porque o valor pertence ao render atual. Chamar setPaes(1) agenda outro
render, mas não reescreve a constante paes da execução que já está em
andamento. Os logs tornam essa ordem visível:
import { useState } from 'react';
export default function BalcaoAgendado() {
const [paes, setPaes] = useState(0);
function adicionar() {
console.log('antes do setPaes :', paes);
setPaes(paes + 1);
console.log('depois do setPaes:', paes);
}
console.log('--- render com paes =', paes);
return (
<div>
<p>Pães de queijo: {paes}</p>
<button onClick={adicionar}>Adicionar</button>
</div>
);
}Leia a sequência com calma. Depois do setPaes(paes + 1), paes ainda vale
0 na linha seguinte. O valor novo só aparece na próxima vez que o React chama
o componente — a linha “render com paes = 1”.
Uma consequência prática disso: chamar dois atualizadores diferentes no mesmo manipulador não gera dois renders. O React junta tudo e redesenha uma vez só.
import { useState } from 'react';
export default function BalcaoDois() {
const [paes, setPaes] = useState(0);
const [sacola, setSacola] = useState(false);
function fecharPedido() {
setPaes(6);
setSacola(true);
console.log('os dois setters já foram chamados');
}
console.log('render:', paes, sacola);
return (
<div>
<p>
{paes} pão(ões), sacola: {String(sacola)}
</p>
<button onClick={fecharPedido}>Fechar meia dúzia</button>
</div>
);
}Dois estados mudaram e houve um render. Esse agrupamento se chama batching: o React enfileira os pedidos do evento e atualiza a tela depois. Na comanda, pão e sacola chegam juntos ao quadro; no código, continuam sendo duas atualizações processadas em lote. Veja mais sobre re-render no React.
Até aqui: valor atual e próximo render
Estado sobrevive entre renders; variável local, não. O setter enfileira o próximo valor e, durante o evento, o código ainda lê o render atual.
Por que três chamadas ao setter somam apenas uma?
Porque as três chamadas calculam a partir do mesmo paes do render atual. No
botão de trio da padaria, a forma mais tentadora é chamar setPaes três vezes:
import { useState } from 'react';
export default function BalcaoTrio() {
const [paes, setPaes] = useState(0);
function adicionarTrio() {
setPaes(paes + 1);
setPaes(paes + 1);
setPaes(paes + 1);
}
console.log('render com paes =', paes);
return (
<div>
<p>Pães de queijo: {paes}</p>
<button onClick={adicionarTrio}>Adicionar trio</button>
</div>
);
}Dois cliques no botão produziram esta sequência:
Dois cliques, seis chamadas de setPaes, e a comanda tem dois pães. Cada
clique somou 1.
O motivo é o da seção anterior. Dentro de adicionarTrio, paes vale 0 nas
três linhas — é a mesma const, congelada naquele render. As três chamadas
viram, na fila do React, “o valor passa a ser 1”, “o valor passa a ser 1”, “o
valor passa a ser 1”. A última ganha, e ela também disse 1.
Experimente você mesmo
Comece mentalmente com quatro pães. Preveja o valor lido pelas três chamadas e o número do próximo render; depois compare com a correção.
Quando usar a forma de função do setter?
Use-a quando o valor novo depende do anterior. O setter recebe uma função; o React passa a ela o valor mais recente da fila, e a função devolve o próximo. O componente muda em um único ponto:
import { useState } from 'react';
export default function BalcaoTrioFuncao() {
const [paes, setPaes] = useState(0);
function adicionarTrio() {
setPaes((anterior) => anterior + 1);
setPaes((anterior) => anterior + 1);
setPaes((anterior) => anterior + 1);
}
console.log('render com paes =', paes);
return (
<div>
<p>Pães de queijo: {paes}</p>
<button onClick={adicionarTrio}>Adicionar trio</button>
</div>
);
}Os mesmos dois cliques, no mesmo lugar:
De 0 para 3, de 3 para 6. Agora as três chamadas se enfileiram como “some 1”, “some 1”, “some 1”, e o React aplica uma depois da outra sobre o resultado anterior. O render continua sendo um só por clique — o que mudou não foi a quantidade de renders, foi o que cada atualização calcula.
A regra prática cabe numa linha: se o valor novo depende do valor atual, use a
forma de função. setPaes(paes + 1) depende. setPaes(0) não depende, e pode
continuar sendo o valor direto.
Até aqui: valor pronto ou função atualizadora
Valor pronto serve para zerar; função atualizadora, para calcular sobre a fila. Nos dois casos, o React controla quando o render acontece.
Por que useState(funcao()) repete o trabalho em todo render?
Porque o JavaScript chama a função antes de entregar o resultado ao useState.
O React ignora resultados novos depois da montagem, mas não impede essa chamada.
Veja isso numa tabela de preços que exige bastante trabalho para ser criada:
export function lerTabelaDePrecos() {
console.log('lendo a tabela de preços...');
const tabela = {};
for (let i = 0; i < 300000; i++) {
tabela['item-' + (i % 500)] = (i % 97) / 10;
}
tabela.paoDeQueijo = 3.5;
return tabela;
}A primeira tentativa chama a função dentro do useState:
import { useState } from 'react';
import { lerTabelaDePrecos } from './tabela.js';
export default function CaixaAvido() {
const [tabela] = useState(lerTabelaDePrecos());
const [paes, setPaes] = useState(0);
return (
<div>
<p>Total: R$ {(paes * tabela.paoDeQueijo).toFixed(2)}</p>
<button onClick={() => setPaes((anterior) => anterior + 1)}>Adicionar</button>
</div>
);
}Montando e clicando três vezes:
Quatro leituras: a da montagem e mais uma em cada um dos três renders que os cliques provocaram. O React só usa o resultado da primeira, mas o JavaScript precisa executar a função antes de passar o valor. O argumento é avaliado sempre, mesmo quando é jogado fora.
A correção são dois caracteres a menos — passar a função em vez de chamá-la:
import { useState } from 'react';
import { lerTabelaDePrecos } from './tabela.js';
export default function CaixaPreguicoso() {
const [tabela] = useState(lerTabelaDePrecos);
const [paes, setPaes] = useState(0);
return (
<div>
<p>Total: R$ {(paes * tabela.paoDeQueijo).toFixed(2)}</p>
<button onClick={() => setPaes((anterior) => anterior + 1)}>Adicionar</button>
</div>
);
}Uma leitura só. Isso se chama inicialização preguiçosa: o React guarda a função e a chama uma única vez, na montagem. Medindo o custo de montar e clicar dez vezes nos dois componentes, num MacBook com Node 24.16.0, média de 5 execuções descartando o aquecimento:
Quase dez vezes. O número exato depende do que a sua função faz — o que interessa é que ele cresce com o número de renders na versão ávida e fica parado na outra.
É melhor usar um objeto ou vários estados?
Agrupe campos que mudam juntos; separe os que mudam por motivos diferentes. A comanda tem cliente, quantidade e sacola. Juntar tudo num objeto é possível, mas o setter traz uma pegadinha:
import { useState } from 'react';
export default function Comanda() {
const [pedido, setPedido] = useState({
cliente: 'Marina',
paes: 0,
sacola: false,
});
function adicionar() {
setPedido({ paes: pedido.paes + 1 });
}
return (
<div>
<p>
{pedido.cliente} — {pedido.paes} pão(ões), sacola:{' '}
{String(pedido.sacola)}
</p>
<button onClick={adicionar}>Adicionar</button>
</div>
);
}O nome da cliente sumiu e a sacola virou undefined. O atualizador substitui
o estado, não mescla campo a campo. Quem passa um objeto com uma chave só está
dizendo que o estado inteiro agora é aquele objeto.
A correção é copiar o que já existia antes de trocar o campo, com o operador spread — e, como o valor novo depende do atual, a forma de função de novo:
function adicionar() {
setPedido((anterior) => ({ ...anterior, paes: anterior.paes + 1 }));
}Repare nos parênteses em volta das chaves: sem eles, a arrow function acharia
que { é o começo de um corpo de função. Essa cópia é obrigatória sempre que o
estado é objeto ou array; a próxima lição explica essa regra por inteiro.
cliente e paes mudam em momentos diferentes, então dois useState podem
deixar o código mais direto. Já campos de um formulário enviado como uma unidade
podem ganhar em ficar juntos.
Quando um valor não deveria virar estado?
Quando ele pode ser calculado a partir de estado ou props que já existem. Guardar essa cópia cria duas versões da mesma verdade. O total da comanda, por exemplo, é quantidade vezes preço, mas parece razoável espelhá-lo em outro estado:
import { useState } from 'react';
const PRECO = 3.5;
export default function CaixaEspelhado() {
const [paes, setPaes] = useState(0);
const [total, setTotal] = useState(0);
function adicionar() {
setPaes((anterior) => anterior + 1);
setTotal((anterior) => anterior + PRECO);
}
function limparComanda() {
setPaes(0);
}
return (
<div>
<p>
{paes} pão(ões) — total R$ {total.toFixed(2)}
</p>
<button id="mais" onClick={adicionar}>Adicionar</button>
<button id="limpar" onClick={limparComanda}>Limpar comanda</button>
</div>
);
}Três cliques em “Adicionar” e um em “Limpar comanda”:
Zero pães, dez e cinquenta a pagar. Quem escreveu limparComanda esqueceu de
zerar o segundo estado, e o cliente vê um valor que não corresponde a nada. Dois
estados que descrevem o mesmo fato sempre acabam divergindo — é questão de
quantas pessoas mexem no arquivo.
A versão certa não tem setTotal. Não tem nem estado total:
import { useState } from 'react';
const PRECO = 3.5;
export default function CaixaDerivado() {
const [paes, setPaes] = useState(0);
const total = paes * PRECO;
return (
<div>
<p>
{paes} pão(ões) — total R$ {total.toFixed(2)}
</p>
<button id="mais" onClick={() => setPaes((anterior) => anterior + 1)}>
Adicionar
</button>
<button id="limpar" onClick={() => setPaes(0)}>
Limpar comanda
</button>
</div>
);
}total é uma variável comum, recalculada em todo render. E recalcular é
barato, porque o componente já ia rodar de qualquer jeito. Antes de criar um
useState, pergunte: dá para calcular isso a partir de outro estado ou de uma
prop? Se der, calcule.
Como corrigir o erro Too many re-renders?
Entregue uma função ao evento em vez de chamar o setter durante o render. O erro
aparece quando se tenta passar um argumento para setPaes desta forma:
import { useState } from 'react';
export default function BalcaoLoop() {
const [paes, setPaes] = useState(0);
return (
<div>
<p>Pães de queijo: {paes}</p>
<button onClick={setPaes(paes + 1)}>Adicionar</button>
</div>
);
}A tela nem chega a aparecer:
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop. at renderWithHooksAgain (/private/tmp/padaria/node_modules/react-dom/cjs/react-dom-client.development.js:7747:17) at renderWithHooks (/private/tmp/padaria/node_modules/react-dom/cjs/react-dom-client.development.js:7665:21) at updateFunctionComponent (/private/tmp/padaria/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19) at beginWork (/private/tmp/padaria/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18)
Node.js v24.16.0
onClick={setPaes(paes + 1)} não entrega a função para o botão: ela chama
setPaes durante o render e entrega o resultado (que é undefined). Chamar o
atualizador durante o render pede outro render, que chama de novo. O React não
espera o computador travar: ele conta essas repetições e aborta ao bater o teto
de 25 — é a constante RE_RENDER_LIMIT do react-dom, e é disso que a
mensagem fala quando diz limits the number of renders.
A correção é embrulhar a chamada numa função que só roda no clique:
- <button onClick={setPaes(paes + 1)}>Adicionar</button>
+ <button onClick={() => setPaes(paes + 1)}>Adicionar</button>O mesmo raciocínio vale para qualquer manipulador com argumento — o assunto está detalhado em eventos no React. E quando o laço nasce de um efeito em vez do render, a mensagem muda para Maximum update depth exceeded.
O que estudar depois de useState?
O próximo passo da trilha, na ordem 9, é atualizar estado sem mutar arrays e objetos. É a continuação direta da comanda: você vai entender por que copiar antes de alterar entrega ao React uma nova referência sem destruir o valor anterior.
Depois vem o formulário controlado,
onde o value do campo nasce do estado e o onChange chama o setter a cada
edição. Assim, contador, objeto e formulário deixam de ser assuntos separados e
viram etapas do mesmo modelo mental.
A ordem completa está na trilha de React, e o guia de React mostra onde cada assunto entra no caminho de quem está saindo do JavaScript puro.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
Posso chamar o useState dentro de um if?
Qual a diferença entre useState e useRef para guardar um valor?
Preciso de useState para um valor que veio por props?
Dá para usar useState num componente de classe?
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 — useState — react.dev
- React — Queueing a Series of State Updates — react.dev



