Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

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.

Rodolfo Mori12 min de leitura

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:

jsx
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>;
}
[ 0, [Function: bound dispatchSetState] ] valor : 0 atualizador: function

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:

jsx
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:

<div><p>Pães de queijo: 0</p><button>Adicionar</button></div> <div><p>Pães de queijo: 1</p><button>Adicionar</button></div> <div><p>Pães de queijo: 2</p><button>Adicionar</button></div>

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:

jsx
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>
  );
}
render com paes = 0 a variável agora vale 1 a variável agora vale 2 na tela: Pães de queijo: 0

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:

jsx
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>
  );
}
--- render com paes = 0 antes do setPaes : 0 depois do setPaes: 0 --- render com paes = 1 na tela: Pães de queijo: 1

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”.

React chama o componente o JSX vira tela o clique chama setPaes(...) React agenda um novo render

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ó.

jsx
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>
  );
}
render: 0 false os dois setters já foram chamados render: 6 true

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:

jsx
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:

render com paes = 0 render com paes = 1 render com paes = 2 na tela: Pães de queijo: 2

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:

jsx
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:

render com paes = 0 render com paes = 3 render com paes = 6 na tela: Pães de queijo: 6

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:

js
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:

jsx
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:

lendo a tabela de preços... lendo a tabela de preços... lendo a tabela de preços... lendo a tabela de preços... na tela: Total: R$ 10.50

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:

jsx
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>
  );
}
lendo a tabela de preços... na tela: Total: R$ 10.50

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:

CaixaAvido 152.8 ms CaixaPreguicoso 15.9 ms

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:

jsx
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>
  );
}
Marina — 0 pão(ões), sacola: false — 1 pão(ões), sacola: undefined — 2 pão(ões), sacola: undefined

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:

jsx
function adicionar() {
  setPedido((anterior) => ({ ...anterior, paes: anterior.paes + 1 }));
}
Marina — 0 pão(ões), sacola: false Marina — 1 pão(ões), sacola: false Marina — 2 pão(ões), sacola: false

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:

jsx
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”:

depois de 3 cliques: 3 pão(ões) — total R$ 10.50 depois de limpar : 0 pão(ões) — total R$ 10.50

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:

jsx
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>
  );
}
depois de 3 cliques: 3 pão(ões) — total R$ 10.50 depois de limpar : 0 pão(ões) — total R$ 0.00

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:

jsx
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:

/private/tmp/padaria/node_modules/react-dom/cjs/react-dom-client.development.js:7747 throw Error( ^

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:

diff
-      <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.

Ver todos os vídeos do canal
  • react
  • usestate
  • estado
  • hooks

Perguntas frequentes

Posso chamar o useState dentro de um if?
Não. O React identifica cada estado pela ordem de chamada dos hooks, e essa ordem precisa ser idêntica em todo render. Hook fica sempre no topo do componente, fora de if, de laço e de função aninhada.
Qual a diferença entre useState e useRef para guardar um valor?
Os dois sobrevivem entre renders, mas só o useState pede um novo render quando muda. Se o valor aparece na tela, é useState. Se ele é um bastidor (o id de um timer, o elemento do DOM), é useRef.
Preciso de useState para um valor que veio por props?
Quase nunca. Copiar props para o estado cria duas versões da verdade, e a cópia congela no valor da primeira renderização. Use a prop direto; se precisar reiniciar o estado quando ela muda, troque a key do componente.
Dá para usar useState num componente de classe?
Não. Hook só funciona em componente de função. Em classe o equivalente é o par this.state e this.setState, que tem regras próprias e não é mais o caminho recomendado para código novo.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. React — useState — react.dev
  2. React — Queueing a Series of State Updates — react.dev

Continue por aqui