Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
Erro resolvidoIntermediáriocódigo testado

Maximum update depth exceeded: o loop de re-render

O erro que trava a aba: setState no render ou useEffect com dependência errada. As quatro causas, cada uma reproduzida com o rastro real do React 19.

Rodolfo Mori10 min de leitura

Maximum update depth exceeded é o React desistindo. Alguma coisa no seu componente pede um render novo toda vez que ele renderiza, e o ciclo não fecha. O erro é o freio de emergência, não a doença — e a doença está sempre em uma de quatro linhas possíveis.

Os exemplos deste artigo são o painel de uma clínica: a lista de consultas do dia, com paciente, horário e status. Tudo rodou num MacBook, no Node 24.16.0 com jsdom 30.0.1 e React 19.2.8 em modo de desenvolvimento. A saída logo abaixo de cada bloco é a saída real dessa execução.

O nome técnico do defeito é ciclo de atualização: renderizar provoca uma mudança de estado que provoca outro render, sem uma condição capaz de encerrar a sequência. Em palavras simples, o componente aciona de novo o mecanismo que acabou de acioná-lo.

O termostato que manda ligar toda vez que liga

Imagine um termostato defeituoso que, ao perceber o aquecedor ligado, envia outro comando para ligar. Cada comando gera a condição que dispara o próximo, e o sistema nunca chega ao estado estável. setState no render, efeito que atualiza a própria dependência e handler executado no JSX criam versões desse feedback no React.

Antes de corrigir o primeiro caso, desenhe uma seta para cada causa: render → setter → render. Depois marque onde deveria existir uma condição de parada. Conte os logs da execução e confirme que a sequência repete o mesmo caminho; na versão corrigida, a seta precisa terminar.

O React usa dois textos diferentes para o mesmo problema, e eles não valem a mesma coisa. Saber qual dos dois apareceu já corta o diagnóstico pela metade.

o que aparece no console de onde veio o React para sozinho?
Error: Maximum update depth exceeded. atualização síncrona encadeada no commit — useLayoutEffect ou ref de callback sim, ele lança e aborta a árvore
Maximum update depth exceeded. This can happen when a component calls setState inside useEffect… atualização encadeada num useEffect comum não — é só um aviso, o loop continua
Error: Too many re-renders. setState chamado durante o próprio render sim, e antes de qualquer efeito rodar

A primeira versão é a que dá nome ao artigo, e é a única lançada como erro de verdade. Ela aparece quando a atualização encadeia dentro do próprio commit, e o caso mais comum é o useLayoutEffect. Aqui, uma tentativa de medir a altura da lista de espera e guardar no estado:

jsx
function AlturaDaAgenda() {
  const lista = useRef(null);
  const [altura, setAltura] = useState(0);

  useLayoutEffect(() => {
    setAltura(altura + 1);
  });

  return <ul ref={lista} style={{ height: altura }}><li>09:00 Marina</li></ul>;
}
renders ate o React abortar: 53 Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops. component stack: at AlturaDaAgenda (file:///private/tmp/agenda-clinica/agenda-layout.mjs:6:17)

Repare na frase inside componentWillUpdate or componentDidUpdate. Você não escreveu classe nenhuma: a mensagem é antiga e fala dos métodos de classe. Traduzida para hooks, ela quer dizer efeito de layoutuseLayoutEffect, que roda de forma síncrona antes de o navegador pintar. Cada setState dele empilha uma atualização em cima da outra, sem folga para o navegador respirar.

Um ref de callback escrito direto no JSX cai no mesmo buraco, e quase ninguém liga um ao outro. Como a função é nova a cada render, o React desliga e religa a ref durante o commit; um setState ali dentro encadeia igualzinho. Trocando o useLayoutEffect do exemplo por ref={() => setAltura((a) => a + 1)}, sai a mesma mensagem nos mesmos 53 renders.

A parte que assusta é o que sobra na tela depois:

jsx
setTimeout(() => {
  console.log('conteudo de #root depois do erro:', JSON.stringify(document.getElementById('root').innerHTML));
  process.exit(0);
}, 500);
conteudo de #root depois do erro: ""

Tela branca. Quando o erro sobe sem nenhum error boundary no caminho, o React desmonta a raiz inteira. É por isso que esse bug costuma chegar como “a página sumiu” e não como “apareceu um erro”.

Causa 1: setState no corpo do componente

O render precisa ser puro: dados entram, JSX sai, e nada mais acontece. Chamar um setState direto no corpo quebra essa regra e agenda um render novo dentro do render que ainda está acontecendo.

jsx
const banco = [
  { id: 1, paciente: 'Marina', status: 'confirmada' },
  { id: 2, paciente: 'Otávio', status: 'cancelada' },
  { id: 3, paciente: 'Rita', status: 'confirmada' },
];

function Painel() {
  const [confirmadas, setConfirmadas] = useState(0);

  // parece inofensivo: só sincronizar o contador com a lista
  setConfirmadas(banco.filter((c) => c.status === 'confirmada').length);

  return <p>{confirmadas} de {banco.length} confirmadas</p>;
}
renders ate o React abortar: 52 Error: Too many re-renders. React limits the number of renders to prevent an infinite loop. component stack: at Painel (file:///private/tmp/agenda-clinica/painel-no-render.mjs:11:41)

Note que a mensagem não é a do título deste artigo. Loop que nasce dentro do render nunca chega a virar Maximum update depth exceeded, porque o React aborta antes de comitar qualquer coisa. Se você recebeu Too many re-renders, procure por chamada de setState fora de evento e fora de efeito.

E o conserto aqui não é envolver em useEffect: é não guardar no estado o que dá para calcular. confirmadas é derivado de banco, então é uma constante comum no corpo da função. Quem quiser entender por que o componente roda de novo a cada mudança de estado encontra o mecanismo completo em re-render no React, e o comportamento do próprio setter em useState.

Causa 2: useEffect sem array de dependências

Sem o segundo argumento, o efeito roda depois de todo render. Se ele mexe no estado, o próximo render dispara o efeito de novo, para sempre.

jsx
function Agenda() {
  const [consultas, setConsultas] = useState([]);

  useEffect(() => {
    setConsultas([{ paciente: 'Marina', horario: '09:00' }]);
  });

  return <p>{consultas.length} consultas hoje</p>;
}
React avisou no render 53: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

Essa mensagem é a que a maioria das pessoas encontra — e é a mais traiçoeira, porque ela não interrompe nada. Chega no console como aviso, o React continua renderizando, e a aba vai esquentando. Dá para medir: basta contar renders e avisos por três segundos.

jsx
let renders = 0;
let avisos = 0;

const erroOriginal = console.error;
console.error = (...args) => {
  if (String(args[0]).includes('Maximum update depth')) avisos++;
};

function Agenda() {
  const [consultas, setConsultas] = useState([]);
  renders++;
  useEffect(() => {
    setConsultas([{ paciente: 'Marina', horario: '09:00' }]);
  });
  return <p>{consultas.length} consultas hoje</p>;
}

createRoot(document.getElementById('root')).render(<Agenda />);

const inicio = Date.now();
setTimeout(() => {
  erroOriginal(
    `Em ${((Date.now() - inicio) / 1000).toFixed(1)} s: ${renders} renders e ${avisos} avisos do React.`
  );
  process.exit(0);
}, 3000);
Em 3.0 s: 73747 renders e 1418 avisos do React.

Setenta e três mil renders em três segundos, e nenhum deles é o último. Repetindo a medição, deu 73.752, 72.473 e 67.413. É esse volume que congela a aba: não existe erro fatal, existe uma thread ocupada até o navegador propor fechar a página.

O conserto é declarar o array de dependências, e quase sempre ele é [] quando o efeito só precisa acontecer na montagem. O que cada posição desse array significa está em useEffect.

Causa 3: um objeto novo na lista de dependências

Essa é a versão adulta da causa 2, e a que mais engana gente experiente. O array de dependências existe, está preenchido, e mesmo assim o efeito roda toda vez — porque o React compara as dependências por identidade, não por conteúdo.

js
function renderizar() {
  return { status: 'confirmada' };
}

const primeiroRender = renderizar();
const segundoRender = renderizar();

console.log('parecem iguais? ', JSON.stringify(primeiroRender) === JSON.stringify(segundoRender));
console.log('são o mesmo objeto?', Object.is(primeiroRender, segundoRender));
console.log('e uma string?     ', Object.is('confirmada', 'confirmada'));
console.log('e um array vazio? ', Object.is([], []));
parecem iguais? true são o mesmo objeto? false e uma string? true e um array vazio? false

Dois objetos com exatamente o mesmo conteúdo são objetos diferentes. String é igual a string; objeto novo nunca é igual a objeto novo. Agora veja o que isso faz num componente que monta o filtro dentro do corpo:

jsx
function ListaDeConsultas() {
  const [consultas, setConsultas] = useState([]);
  const filtro = { status: 'confirmada' };

  useEffect(() => {
    setConsultas(banco.filter((c) => c.status === filtro.status));
  }, [filtro]);

  return <p>{consultas.length} consulta(s)</p>;
}
render 53 -> Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

O array de dependências está lá, e o efeito roda infinitamente do mesmo jeito. Vale para objeto, array, função declarada no corpo e para qualquer coisa criada durante o render. Quando o valor precisa mesmo ser um objeto, ele vira caso de useMemo e useCallback; quando não precisa, depender do campo primitivo é mais barato e mais legível.

Causa 4: onClick={confirmar()} em vez de onClick={confirmar}

Um par de parênteses a mais e o manipulador deixa de ser passado para ser executado — durante o render, que é exatamente o lugar proibido.

jsx
function Confirmacao() {
  const [status, setStatus] = useState('pendente');

  function confirmar() {
    setStatus('confirmada');
  }

  return <button onClick={confirmar()}>Confirmar consulta ({status})</button>;
}
renders ate o React abortar: 52 Error: Too many re-renders. React limits the number of renders to prevent an infinite loop. component stack: at Confirmacao (file:///private/tmp/agenda-clinica/agenda-onclick.mjs:6:31)

Aqui aparece um detalhe que quase ninguém espera. O setStatus só muda o valor na primeira vez: da segunda em diante ele já vale 'confirmada'. Você diria que o React ia parar de renderizar, porque o estado não muda mais. Não para:

jsx
function confirmar() {
  setStatus('pendente'); // mesmo valor que ja esta no estado
}
renders ate o React abortar: 52 Error: Too many re-renders. React limits the number of renders to prevent an infinite loop. component stack: at Confirmacao (file:///private/tmp/agenda-clinica/agenda-mesmo-valor.mjs:6:31)

Cinquenta e dois renders de novo, com o valor idêntico. A comparação que evita render desnecessário só vale para setState chamado fora do render: aí o React confere na hora se o valor novo é igual ao atual e nem agenda a atualização. Chamado durante o render, o setState pula essa conferência e força outra passagem de qualquer jeito. É por isso que o padrão de estado derivado só funciona com uma guarda explícita (if (valorAtual !== valorNovo) setValor(valorNovo)).

Em quantos renders o React desiste

Os três limites estão escritos no próprio react-dom-client.development.js do React 19.2.8: RE_RENDER_LIMIT = 25, NESTED_UPDATE_LIMIT = 50 e NESTED_PASSIVE_UPDATE_LIMIT = 50. O que se mede na execução é maior que isso, porque o React repete a passagem de render que falhou antes de desistir. No setState no corpo dá para ver o mecanismo inteiro: imprimindo o estado lido a cada chamada, sai 0,1,2,…,25 e depois 0,1,2,…,25 outra vez — a passagem tem 26 renders (o inicial mais os 25 do limite) e roda duas vezes. Daí os 52.

caso limite interno chamadas do componente até o React reagir
setState no corpo do render 25 re-renders na mesma passagem 52
setState em useLayoutEffect 50 atualizações encadeadas 53
setState em useEffect 50 atualizações encadeadas 53, e aí só avisa

Os números vieram de um contador incrementado dentro do componente, e se repetiram idênticos em três execuções seguidas. A lição prática não é decorar 52 ou 53: é que o React reage em dezenas de renders, não em milhares. Se o seu contador passou de trinta, o loop já está formado.

Achando o culpado antes de a aba congelar

Quando o loop está em useEffect, o navegador trava antes de você conseguir abrir o DevTools. A saída é instalar um limite seu, que estoura antes do React e aponta o componente pelo nome.

jsx
function useLimiteDeRenders(nome, limite = 30) {
  const contador = useRef(0);
  contador.current += 1;
  if (contador.current > limite) {
    throw new Error(`${nome} renderizou ${contador.current} vezes seguidas — provável loop`);
  }
}
ListaDeConsultas renderizou 32 vezes seguidas — provável loop component stack: at ListaDeConsultas (file:///private/tmp/agenda-clinica/limite-renders.mjs:13:3)

Estourou em 32 — o React ainda repete a passagem que falhou, por isso passa um pouco do limite pedido. O importante é que o erro é seu, com o nome do componente, e chega em milissegundos em vez de travar a aba. O useRef guarda o contador sem provocar render nenhum, que é justamente o que se precisa aqui; o mecanismo está em useRef.

Achado o componente, falta descobrir qual dependência mudou. Esse hook compara o array de dependências com o da passagem anterior e imprime a diferença:

jsx
function useEfeitoDepurado(nome, efeito, deps) {
  const anteriores = useRef(deps);
  deps.forEach((dep, i) => {
    if (!Object.is(dep, anteriores.current[i])) {
      console.log(`[${nome}] dep ${i} mudou:`, anteriores.current[i], '->', dep);
    }
  });
  anteriores.current = deps;
  useEffect(efeito, deps);
}
[buscarConsultas] dep 0 mudou: { status: 'confirmada' } -> { status: 'confirmada' } [buscarConsultas] dep 0 mudou: { status: 'confirmada' } -> { status: 'confirmada' } [buscarConsultas] dep 0 mudou: { status: 'confirmada' } -> { status: 'confirmada' } [buscarConsultas] dep 0 mudou: { status: 'confirmada' } -> { status: 'confirmada' }

Essa linha é o diagnóstico inteiro numa frase: a dependência 0 “mudou” de { status: 'confirmada' } para { status: 'confirmada' }. Ler isso no console resolve em segundos um bug que costuma consumir uma tarde.

A correção de cada causa

causa mensagem que ela produz correção
setState no corpo Too many re-renders calcular no render, sem estado
useEffect sem array aviso de Maximum update depth declarar as dependências, quase sempre []
objeto novo nas dependências aviso de Maximum update depth depender do campo primitivo
onClick={handler()} Too many re-renders passar a função: onClick={handler}

As quatro correções, aplicadas ao mesmo painel da clínica:

jsx
// causa 1: o que dá para calcular não vira estado
function Painel({ consultas }) {
  const confirmadas = consultas.filter((c) => c.status === 'confirmada').length;
  return <p>{confirmadas} de {consultas.length} confirmadas</p>;
}

// causas 2 e 3: array de dependências com valor primitivo
function Lista({ status }) {
  const [consultas, setConsultas] = useState([]);

  useEffect(() => {
    setConsultas(banco.filter((c) => c.status === status));
  }, [status]);

  return <ul>{consultas.map((c) => <li key={c.id}>{c.paciente}</li>)}</ul>;
}

// causa 4: onClick recebe a função, não a chamada
function Confirmacao() {
  const [status, setStatus] = useState('pendente');
  return <button onClick={() => setStatus('confirmada')}>{status}</button>;
}
HTML: <div><p>2 de 3 confirmadas</p><ul><li>Marina</li><li>Rita</li></ul><button>pendente</button></div> renders: {"Painel":1,"Lista":2,"Confirmacao":1} depois do clique: confirmada renders: {"Painel":1,"Lista":2,"Confirmacao":2}

Painel renderizou uma vez. Lista renderizou duas — a montagem e a chegada dos dados — e parou. O clique fez Confirmacao renderizar mais uma, e só. É essa a assinatura de um componente saudável: número pequeno, e um número que para de crescer.

O próximo passo

Se o rastro aponta para um componente que você não reconhece, o problema provavelmente está num hook compartilhado ou num contexto. E se o console mostrar outra mensagem além dessas, vale conferir Rendered more hooks than during the previous render, que é o outro erro clássico de hook fora do lugar. Para revisar a ordem de estudo dos hooks antes de voltar ao código, o guia de React mostra onde estado, efeito e memorização entram na sequência.

Prefere aprender em vídeo?

Tem uma aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • react
  • useeffect
  • loop infinito
  • setstate
  • erro

Perguntas frequentes

Por que às vezes a aba trava e nem aparece erro vermelho?
Porque o loop está num useEffect comum. Nesse caminho o React só imprime um aviso no console e continua renderizando. Na build de produção nem o aviso existe: a string da mensagem não está no arquivo compilado.
Colocar array de dependências vazio resolve sempre?
Para o loop, sim, mas só porque o efeito passa a rodar uma vez. Se o efeito realmente depende de um valor que muda, o array vazio troca um bug por outro: a tela para de acompanhar o dado. O certo é listar valores estáveis, de preferência primitivos.
useMemo resolve o problema da dependência que muda toda hora?
Resolve quando o objeto precisa mesmo existir, porque o useMemo mantém a mesma referência entre renders. Só que na maioria dos casos é mais simples depender do campo primitivo dentro do objeto e não guardar objeto nenhum.
Qual a diferença entre Too many re-renders e Maximum update depth exceeded?
A primeira acontece durante o render, antes de qualquer efeito, e o limite é de 25 re-renders na mesma passagem. A segunda acontece depois do commit, quando uma atualização encadeia na outra, e o limite é de 50 níveis.
O erro pode vir de um componente que não é o que aparece no rastro?
Pode. O rastro mostra quem chamou a atualização de estado, e esse estado pode morar num contexto ou num hook customizado compartilhado. Quando o nome do rastro não faz sentido, procure o setState dentro dos hooks que aquele componente usa.

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 com jsdom 30.0.1 e React 19.2.8, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. React — You Might Not Need an Effect — react.dev
  2. React — Removing Effect Dependencies — react.dev
  3. React — Keeping Components Pure — react.dev

Continue por aqui