Como Corrigir "Maximum Update Depth Exceeded" no React
Corrija o erro React maximum update depth exceeded com ajustes de uma linha para loops de render, deps de efeito, handlers e throttling.
“Maximum update depth exceeded” significa que o seu componente está preso em um loop infinito de renderização: uma atualização de estado dispara uma nova renderização que dispara a mesma atualização de estado novamente, e o React aborta o processo após exceder 50 atualizações aninhadas (seu NESTED_UPDATE_LIMIT) para impedir que o navegador congele.
Se você já assistiu à aba travar enquanto a mesma linha vermelha se acumulava no console, sabe o quanto aquela parede de texto parece inútil à primeira vista. A boa notícia é que a correção quase sempre é uma alteração de uma única linha, assim que você identifica qual padrão foi violado.
Este guia cobre o conjunto completo de causas, incluindo o caso dos handlers de alta frequência, com um antes/depois pronto para copiar e colar para cada situação, uma tabela de consulta por sintoma para direcioná-lo rapidamente e as ferramentas para localizar o setter problemático em menos de um minuto. A causa raiz é idêntica em componentes de função e de classe: um loop de atualização que nunca se estabiliza.
Principais Conclusões
- O erro é um loop infinito de renderização; o React o interrompe depois que a contagem de atualizações aninhadas ultrapassa 50 (
NESTED_UPDATE_LIMITno código-fonte do reconciler). onClick={handleClick()}chama a função durante a renderização e agenda uma atualização de estado a cada renderização. PasseonClick={handleClick}, eonClick={() => handleClick(id)}quando precisar de argumentos.- A correção mais reaproveitável de todas é a atualização funcional (
setCount(prev => prev + 1)), que permite remover aquele estado do array de dependências do effect e quebra o ciclo de leitura-e-escrita. - O
useCallbackestabiliza a identidade de uma função, não a frequência com que ela é executada, portanto não resolverá loops causados por handlers de alta frequência comoonScrollou oonDragMovedo dnd-kit; aplique throttle ou debounce na atualização de estado. - O erro
#185de componentes de classe é lançado tanto em desenvolvimento quanto em produção, mas a variante douseEffecté apenas um aviso exclusivo de desenvolvimento. Em produção, esse loop roda sem exceção alguma e sem qualquer sinal no console.
Consulta: sintoma → causa → correção
Encontre o sintoma que corresponde ao que você está observando e vá direto para a correção pertinente.
| Sintoma | Causa | Correção |
|---|---|---|
| Loop sem nenhum clique, começa na montagem | onClick={fn()} chama o setter durante a renderização | Passe onClick={fn} |
setState no nível superior do componente | Atualização de estado no caminho de renderização | Mova para um handler ou effect |
| Effect dispara a cada renderização | Deps ausentes/incorretas, ou objeto/array inline nas deps | Corrija as deps + useMemo/useCallback |
| Effect lê e escreve o mesmo estado | O estado está nas próprias deps | Updater funcional; remova a dep |
| Loop apenas ao arrastar/rolar/redimensionar | setState a cada evento de alta frequência | Aplique throttle/debounce na atualização |
| Pai e filho sobrescrevem um ao outro | Sincronização bidirecional de estado | Torne o fluxo unidirecional |
Discover how at OpenReplay.com.
Correções 1 e 2: referências de handlers e setState no caminho de renderização
Os dois ganhos mais rápidos estão em como você conecta os handlers e onde chama o setter. onClick={handleClick()} chama a função durante a renderização e agenda uma atualização de estado a cada renderização; você quase sempre quer onClick={handleClick}, e onClick={() => handleClick(id)} quando precisa passar argumentos.
// BAD: acceptTerms runs during render, every render
<input type="checkbox" onChange={acceptTerms()} />
// GOOD: pass the reference; wrap in an arrow to pass args
<input type="checkbox" onChange={acceptTerms} />
<button onClick={() => selectItem(item.id)}>Select</button>
O mesmo loop acontece quando você chama um setter diretamente no corpo do componente. Atualizações de estado pertencem a um manipulador de eventos ou a um effect, nunca ao caminho de renderização.
// BAD: runs on every render → loop
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>;
}
// GOOD: update in response to an event
const increment = () => setCount(c => c + 1);
Correções 3, 4 e 5: loops de dependências em effects
A maioria dos loops em effects vem de dependências que mudam de identidade ou de um effect que escreve o estado que lê. Um literal de objeto ou array escrito inline em um array de dependências ganha uma nova identidade a cada renderização, então o effect é reexecutado a cada renderização; encapsule-o com useMemo (objetos/arrays) ou useCallback (funções) para que sua referência permaneça estável.
// BAD: options is a new object each render → effect re-runs forever
const options = { limit: 10, sort: 'date' };
useEffect(() => { search(query, options).then(setResults); }, [query, options]);
// GOOD: memoize so the reference is stable
const options = useMemo(() => ({ limit: 10, sort: 'date' }), []);
A correção mais reaproveitável de todas é a atualização funcional. Quando um effect lê e escreve o mesmo estado, setCount(prev => prev + 1) lê o valor anterior a partir do argumento do updater em vez do closure, o que permite remover aquele estado do array de dependências e quebra o loop.
// BAD: count is read and written, and it's in deps
useEffect(() => { setCount(count + 1); }, [count]);
// GOOD: functional updater removes the dependency
useEffect(() => { setCount(prev => prev + 1); }, []);
Para uma função da qual um effect depende, ou você a encapsula em useCallback com as deps corretas, ou a move para dentro do effect: uma função declarada dentro do effect é criada uma vez por execução e não precisa ser uma dependência.
Correção 6: aplique throttle em handlers de alta frequência
O useCallback estabiliza a identidade de uma função entre renderizações, mas não altera com que frequência a função é executada, portanto não resolverá um loop infinito causado por um handler de alta frequência como onScroll, onMouseMove, onResize ou o onDragMove do dnd-kit. Esses eventos disparam dezenas de vezes por segundo, e cada setState agenda mais uma renderização. Em vez disso, aplique throttle ou debounce na atualização de estado.
// BAD: fires dozens of times/sec while dragging
const handleDragMove = (event) => setDragPreview(compute(event));
// GOOD: cap the update rate; useCallback alone won't help
import { throttle } from 'lodash';
const handleDragMove = throttle((event) => setDragPreview(compute(event)), 100);
O throttle do lodash limita quantas vezes a função encapsulada pode disparar dentro de uma determinada janela; o requestAnimationFrame nativo ou um debounce também funcionam. O ponto é o controle de frequência, não a estabilidade da referência.
Correção 7: loops de sincronização entre pai e filho
A propagação bidirecional de estado (um effect no filho que chama o setter do pai, que rerenderiza o filho, que dispara o effect novamente) é outro loop clássico. Eleve o estado a um único dono e mantenha o fluxo de dados unidirecional, ou transforme o valor no componente pai em vez de sincronizá-lo de volta por meio de um effect.
Localize o problema rapidamente (e o caso dos componentes de classe)
Trabalhe de cima para baixo: leia o stack trace até o setter nomeado no topo do loop, depois abra o Profiler do React DevTools para encontrar o componente que rerenderiza sem parar. Adicione o why-did-you-render (testado com o React 19, exclusivo de desenvolvimento e não testado com o React Compiler) para ver qual prop ou estado mudou de identidade. Capture esses casos já no lint: a partir do eslint-plugin-react-hooks 7.x, o plugin traz regras dedicadas set-state-in-render e set-state-in-effect que detectam os dois gatilhos mais comuns antes mesmo de você executar a aplicação. Ambas fazem parte do preset padrão recommended, então atualizar o plugin já basta para ativá-las; o recommended-latest apenas adiciona por cima as regras experimentais do compilador. A regra set-state-in-render dispara quando um componente define estado durante a renderização sem nada protegendo a chamada, que é exatamente o formato que degenera em um loop.
O erro tem a mesma causa raiz tanto em componentes de função quanto de classe; em componentes de classe, ele normalmente significa chamar setState durante a renderização ou incondicionalmente dentro de componentDidUpdate. Fique atento também ao ambiente: o erro #185 de componentes de classe é lançado tanto em desenvolvimento quanto em produção, mas a variante do useEffect é apenas um aviso de desenvolvimento. No código-fonte do reconciler, a verificação de atualização passiva aninhada fica dentro de uma guarda exclusiva de desenvolvimento e registra no console em vez de lançar uma exceção, de modo que um build de produção executa esse loop de effect sem exceção, sem error boundary e sem qualquer sinal no console, manifestando-se apenas como uma aba congelada.
Essa lacuna em produção é onde o loop se torna caro de depurar. O console mostra o erro, mas não qual interação o causou, e no caso de loops em effects pode não mostrar erro algum. Ferramentas de session replay como o OpenReplay capturam o erro do console, quando presente, junto com a sequência de ações do usuário que o precedeu, de modo que um simples stack trace (ou um congelamento silencioso) se transforma em um caso reproduzível clique a clique que você pode reproduzir contra as correções acima.
Uma vez que você associe seu sintoma a uma linha da tabela, a mudança geralmente é de uma única linha: uma referência em vez de uma chamada, um useMemo em torno de um objeto, ou um updater funcional que permite dispensar uma dependência. Configure o exhaustive-deps junto com as novas regras set-state-in-* para que o próximo loop quebre sua etapa de lint em vez do navegador dos seus usuários.
Perguntas Frequentes
Qual é a diferença entre a variante deste erro no useEffect e o erro #185 do React?
São duas mensagens distintas com comportamentos de execução diferentes. O erro #185 é a formulação para componentes de classe, mencionando setState dentro de componentWillUpdate ou componentDidUpdate, e é lançado tanto em desenvolvimento quanto em produção. A variante do useEffect é um aviso separado, exclusivo de desenvolvimento, vindo do código-fonte do reconciler e protegido por uma guarda de desenvolvimento, de modo que, em produção, o loop do effect roda sem exceção, sem error boundary e sem qualquer sinal no console.
Por que adicionar useCallback não interrompe meu loop infinito durante o arraste ou a rolagem?
Porque o useCallback estabiliza a identidade de uma função entre renderizações, mas não altera com que frequência essa função é executada. Um handler de alta frequência como onScroll, onMouseMove ou o onDragMove do dnd-kit dispara dezenas de vezes por segundo, e cada setState agenda mais uma renderização, independentemente de a referência da função estar memoizada. A correção é o controle de frequência: aplique throttle ou debounce na própria atualização de estado, usando o throttle do lodash, um debounce ou requestAnimationFrame.
A forma de updater funcional sempre me permite remover o estado do array de dependências de um effect?
Apenas quando a única necessidade que o effect tem daquele estado é calcular o próximo valor. Escrever setCount(prev => prev + 1) lê o valor anterior a partir do argumento do updater em vez do closure, então count pode sair do array de dependências e o ciclo de leitura-e-escrita é quebrado. Se o effect também lê aquele estado para outra lógica, como ramificações condicionais ou repassá-lo a outra função, você ainda precisa dele como dependência e deve quebrar o loop de outra maneira.
Por que o erro aparece em desenvolvimento, mas meu build de produção apenas congela silenciosamente?
Porque a guarda de atualização passiva aninhada da variante do useEffect está envolvida por uma verificação exclusiva de desenvolvimento no reconciler do React, então ela só emite um aviso no console durante o desenvolvimento. Em um build de produção essa guarda não é executada, o que significa que o mesmo loop de effect é executado sem erro lançado e sem mensagem no console, manifestando-se apenas como uma aba congelada ou renderizações descontroladas. O erro #185 de componentes de classe é diferente e é lançado em ambos os ambientes.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k