Se você já ficou intrigado com o famoso aviso react-hooks/exhaustive-deps — ou pior, já o
silenciou só para seguir em frente — este artigo é o seu ponto de virada.
Ao final deste mergulho profundo, você vai:
- entender completamente o que os efeitos do React realmente são
- aprender a eliminar bugs sutis
- depurar lógica de efeito quebrada com confiança
- evitar closures obsoletas e loops infinitos
- dominar como aplicar otimizações avançadas ao
useEffect - rastrear qualquer problema de
useEffectaté a origem e resolvê-lo corretamente
O hook useEffect é uma das APIs mais mal compreendidas do React moderno, e até desenvolvedores
experientes cometem erros com ele. Aliás, a queda do Dashboard da Cloudflare em 12 de setembro de
2025 — como a própria Cloudflare explicou — teve origem no uso incorreto de useEffect no
front-end deles.
Por que isso acontece? Principalmente porque a maioria dos tutoriais e cursos explica como usar o hook, mas raramente esclarece por que ele existe, que problema resolve e qual é o modelo conceitual por trás do seu comportamento — abrindo espaço para os usos incorretos mais comuns.
Este artigo constrói um modelo mental claro desde a base, conectando fundamentos de ciência da computação à arquitetura do React e ao seu fiber scheduler. Vamos começar de onde tudo parte: os efeitos colaterais.
O Conceito Central: Efeitos Colaterais
Em ciência da computação, uma função tem um efeito primário: ler entradas e retornar um valor.
Qualquer coisa além disso é um efeito colateral, como:
- alterar variáveis externas
- lançar exceções
- realizar I/O
- fazer requisições de rede
- mutações no DOM
- interagir com APIs do navegador
Efeitos colaterais não são completamente "ruins", mas tornam o raciocínio mais complexo. É por isso que o paradigma da Programação Funcional incentiva funções puras: previsíveis, testáveis, fáceis de compor.
O React também adota a pureza em seu processo de renderização. A documentação oficial é clara:
O processo de renderização do React deve ser sempre puro. Componentes devem apenas retornar seu JSX, e não alterar nenhum objeto ou variável que já existia antes da renderização — isso os tornaria impuros!
A pureza é essencial porque o React pode re-renderizar seus componentes:
- mais vezes do que você espera
- fora de ordem
- em lotes
- em desenvolvimento (double-render do Strict Mode — veja como o StrictMode melhora a qualidade do código)
- durante transições
- durante retentativas de suspense
O React precisa poder re-executar componentes com segurança a qualquer momento. Mas aplicações web precisam interagir com o mundo exterior. Então, como combinamos pureza com interatividade?
O propósito dos Effects no React
Event handlers são o primeiro mecanismo natural para efeitos colaterais:
<button onClick={handleClick}>Click</button>Handlers são definidos durante a renderização, mas executados depois da renderização, e apenas em resposta a uma interação do usuário. Por isso, eles não quebram a pureza.
Mas às vezes um componente precisa se sincronizar com sistemas externos porque ele renderizou, não porque o usuário clicou em algo:
- atualizar
document.title - interagir com
localStorage - anexar event listeners
- gerenciar conexões WebSocket
- rolar até um elemento
- integrar com APIs do navegador
Esses são efeitos colaterais que surgem da fase de renderização do componente, e devem ser
agendados como effects. É exatamente por isso que o useEffect existe.
O React garante pureza durante a renderização, e agenda os effects para rodar depois da renderização, na fase de commit.
A mecânica fundamental do useEffect
Agora que sabemos o verdadeiro propósito do hook, vamos entender sua sintaxe e execução.
useEffect(setup, deps?)Ele retorna undefined e aceita dois argumentos:
setup: uma função contendo a lógica do seu Effect, que pode ou não retornar uma função de limpeza (veremos isso adiante).deps(opcional): um array de dependências.
O effect executa a função setup depois que o componente renderiza. Por padrão, ele rodaria a
cada renderização, mas raramente queremos isso — por isso usamos o array de dependências na maior
parte das vezes, mesmo quando ele está vazio. Veja como o effect responde a cada formato desse
argumento:
Array de Dependências (deps) | Comportamento do Effect |
|---|---|
| Omitido | Roda depois de toda renderização. |
[] (Array Vazio) | Roda o effect só depois da primeira renderização, e a limpeza quando o componente desmonta. |
[var1, var2] | Roda depois da primeira renderização e sempre que var1 ou var2 mudam. |
Como o array de dependências é comparado
A checagem de mudança no array de dependências é feita usando Object.is(). Se um objeto, array
ou função é recriado a cada renderização — comum quando definido dentro do componente — ele será
considerado diferente mesmo que seus campos e valores sejam idênticos, e o effect vai rodar de
novo. Isso acontece porque objetos e funções são Tipos por Referência em JavaScript: eles não
armazenam um valor, apenas uma referência a um local de memória, que é novo a cada criação.
Um dos erros mais comuns e perigosos é acreditar que podemos preencher o array de dependências como quisermos. Pelo contrário: a lista de dependências deve ser ditada pelo próprio Effect — ela precisa conter todo valor reativo usado dentro dele: toda prop, estado e função declarados dentro do componente.
Para evitar bugs causados por uma lista de dependências mal preenchida, nosso maior aliado é um linter configurado para React. Ele nos diz exatamente como a lista deve ser preenchida, com base no código presente no nosso Effect. Seus avisos não devem ser suprimidos — pelo contrário: a documentação oficial do React nos diz para tratá-los como erros de compilação, dado o alto risco de bugs por uso incorreto do array de dependências. É tentador achar que uma dependência pode simplesmente ser removida; na prática, removê-la exige alterar o código ao redor para "provar" ao linter que aquele valor deixou de ser reativo.
Antes de partirmos para remover dependências corretamente, vamos fechar a explicação da sintaxe do
useEffect com sua última peça, não menos importante: a função de limpeza.
Se um Effect roda múltiplas vezes — criando inscrições ou listeners, por exemplo — precisamos de uma
forma de cancelar a inscrição anterior ou remover o listener anterior, ou corremos o risco de
vazamentos de memória e outros comportamentos inesperados. Para isso, a função setup pode
retornar outra função: a função de limpeza.
useEffect(() => {
// código de setup
return () => {
// Esta é a função de limpeza.
// Ela roda para "desfazer" o setup anterior.
}
}, [deps])A função de limpeza roda (1) antes de cada nova execução do Effect, a partir da segunda, e (2) uma última vez quando o componente é desmontado — garantindo que o effect anterior seja encerrado corretamente antes que um novo comece.
Estratégias para remover dependências do Effect
Às vezes uma lista de dependências parece errada, ou um valor parece mudar com mais frequência do que deveria — sendo recriado a cada renderização, por exemplo. Isso pode realmente acontecer, mas uma dependência nunca pode ser removida abruptamente sem alterar o código ao redor de acordo.
O primeiro passo é verificar se o Effect realmente é um Effect, ou se deveria ser um Event Handler:
Depois de ter certeza de que a lógica realmente pertence ao useEffect, verifique se ela realiza
apenas um processo de sincronização. Isso evita que dependências de um processo diferente
causem re-execuções desnecessárias.
Com isso resolvido, voltamos nossa atenção aos valores reativos que o Effect de fato referencia. Isso revela alguns cenários distintos, cada um com sua própria estratégia de solução. Os três primeiros tentam evitar objetos e funções dentro do array de dependências.
Cenário 1: Objetos ou funções sem valores reativos
A primeira coisa a verificar é se essas entidades dependem de algum valor reativo (props ou estado). Se não dependem, podemos mover sua declaração para fora do escopo do componente, dando a elas uma referência estável, tratada como não reativa.

Como esperado, o linter reporta um problema tanto na declaração do objeto quanto na da função:


Isso mostra por que um linter bem configurado — especificamente a regra react-hooks/exhaustive-deps
— é tão importante. Ele sugere duas correções comuns: mover a inicialização para dentro do Effect,
ou envolvê-la em useMemo() / useCallback().
Mover a inicialização para dentro do Effect resolve o aviso, mas adiciona trabalho desnecessário a
cada execução. A correção com useMemo() / useCallback() é mais interessante, e vamos detalhá-la
no Cenário 3.
Neste caso específico — objetos e funções que não dependem de valores reativos — a melhor solução é a mais simples: mover a declaração completamente para fora do escopo do componente.

Cenário 2: Usando propriedades específicas de objetos complexos
Quando um Effect precisa apenas de um subconjunto das propriedades de um objeto maior — digamos, uma
prop user — depender do objeto inteiro é ineficiente: sua referência muda em quase toda
re-renderização, então o Effect roda de novo sem necessidade. A correção é a desestruturação:
extrair apenas os valores primitivos que o Effect realmente precisa, e listar apenas esses no array
de dependências.
type GoodComponentExampleProps = {
settings: Settings
}
function GoodComponentExample({ settings }: GoodComponentExampleProps) {
const { trackMouse } = settings // ✅ desestrutura o primitivo que precisamos
useEffect(() => {
if (!trackMouse) return // ✅
const handleMouseMove = (event: MouseEvent) => {
console.log(`posição do mouse: { x: ${event.clientX}, y: ${event.clientY} }`)
}
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [trackMouse]) // ✅ não [settings] — depende só do necessário
return <div>GoodComponentExample</div>
}Se o Effect dependesse do objeto settings inteiro, ele rodaria toda vez que qualquer uma de suas
propriedades mudasse. Ao desestruturar e depender apenas do primitivo trackMouse, evitamos isso.
Cenário 3: Referências instáveis criadas durante a renderização
Se nenhuma das estratégias anteriores se aplica, o problema provavelmente é uma referência
instável: um objeto literal ({...} ou [...]) ou uma função declarada diretamente no corpo do
componente. Toda re-renderização cria uma referência de memória totalmente nova para ela, então um
Effect que depende dela roda de novo sem necessidade — e pode até causar um loop infinito.
A estratégia é memoizar o valor para manter sua referência estável entre renderizações:
useMemopara objetos, arrays ou valores calculados.useCallbackpara funções.
O useMemo executa sua função calculateValue e memoiza o valor retornado; o useCallback apenas
memoiza a definição da função, garantindo que o Effect sempre receba a mesma referência estável. A
função passada ao useMemo roda durante a renderização, então precisa ser pura e não receber
argumentos — dependendo inteiramente do seu próprio array de dependências para qualquer valor
reativo que precisar.
Cenário 4: Lendo um valor reativo sem "reagir" a ele
Às vezes um Effect precisa do valor mais recente de uma variável reativa, mas deve rodar de novo apenas quando uma dependência diferente, não relacionada, mudar. Incluir essa variável no array de dependências força re-execuções desnecessárias.
A correção certa depende da versão do React:
React 19.2 ou mais recente — useEffectEvent
A solução moderna é o useEffectEvent, que extrai lógica não reativa para fora do Effect. A função
que ele retorna é garantidamente estável — sua referência nunca muda — então ela não precisa entrar
no array de dependências.
type ChatProps = {
roomId: number
notificationCount: number
}
function Chat({ roomId, notificationCount }: ChatProps) {
const onVisit = useEffectEvent((visitedRoomId) => {
console.log(visitedRoomId, notificationCount)
})
useEffect(() => {
onVisit(roomId)
}, [roomId]) // ✅ todas as dependências declaradas
return <div>Chat Room {roomId}</div>
}A documentação oficial lista três ressalvas importantes:
Ressalvas
- Só chame dentro de Effects. Effect Events só devem ser chamados dentro de Effects. Defina-os logo antes do Effect que os usa; não os passe para outros componentes ou hooks. O linter
eslint-plugin-react-hooks(6.1.1+) reforça essa regra.- Não é um atalho para dependências. Não use
useEffectEventpara evitar especificar dependências — isso esconde bugs e dificulta o entendimento do código. Prefira dependências explícitas, ou refs para comparar valores anteriores quando necessário.- Apenas para lógica não reativa. Use
useEffectEventsó para extrair lógica que não depende de valores que mudam.
Antes do React 19.2 — useRef + useCallback
Para versões mais antigas do React, o padrão estabelecido é guardar o valor reativo mais recente em
um useRef, lido a partir de um handler useCallback estável. Isso exige um useEffect separado,
cuja única função é manter a ref atualizada, para que o Effect principal sempre leia o dado atual
sem depender dele.
type ChatProps = {
roomId: number
notificationCount: number
}
function Chat({ roomId, notificationCount }: ChatProps) {
const notificationCountRef = useRef(notificationCount)
useEffect(() => {
notificationCountRef.current = notificationCount
}, [notificationCount])
const onVisit = useCallback((currNotificationCount: number, roomId: number) => {
console.log(currNotificationCount, roomId)
}, [])
useEffect(() => {
onVisit(notificationCountRef.current, roomId)
}, [onVisit, roomId])
return <div>Chat Room {roomId}</div>
}Uma nota importante sobre busca de dados
Embora tecnicamente possível, a própria documentação do React desaconselha usar useEffect
para busca de dados em aplicações modernas:
Escrever chamadas de
fetchdentro de Effects é uma forma popular de buscar dados, especialmente em apps totalmente client-side. Essa é, porém, uma abordagem bastante manual, com desvantagens significativas:
- Effects não rodam no servidor. O HTML renderizado no servidor inclui apenas um estado de carregamento sem dados — o cliente precisa baixar todo o JavaScript e renderizar o app só para descobrir que precisa carregar dados. Pouco eficiente.
- Buscar dados diretamente em Effects facilita criar "cascatas de rede." O componente pai renderiza, busca dados, renderiza os filhos, e só então eles começam a buscar seus próprios dados — bem mais lento do que buscar tudo em paralelo.
- Buscar diretamente em Effects geralmente significa sem pré-carregamento ou cache. Se um componente desmonta e monta de novo, ele busca os dados de novo.
- Não é muito ergonômico. Há bastante código repetitivo para evitar bugs como condições de corrida.
Isso não é específico do React — se aplica a buscar dados na montagem com qualquer biblioteca. Recomendamos:
- Se você usa um framework, use o mecanismo de busca de dados dele. Frameworks React modernos têm mecanismos de busca integrados e eficientes, que evitam os problemas acima.
- Caso contrário, use ou construa um cache client-side. TanStack Query, useSWR e React Router 6.4+ são opções populares. Você também pode construir o seu — nesse caso, ainda usaria Effects por baixo dos panos, além de lógica para deduplicar requisições, cachear respostas e evitar cascatas.
Bibliotecas como essas lidam com sincronização, cache, revalidação e limpeza de forma muito mais
eficiente e robusta do que um useEffect feito à mão, abstraindo esse caso de uso específico.
Considerações finais
Dominar o useEffect não é sobre decorar regras — é sobre construir um modelo mental.
Effects existem porque o React precisa manter a renderização pura. Effects rodam depois do commit para que o React possa se sincronizar com sistemas externos com segurança.
Quando você entende:
- pureza
- valores reativos
- correção do array de dependências
- referências estáveis
- comportamento de limpeza
- padrões de sincronização
- estratégias modernas de busca de dados
...tudo sobre useEffect se torna previsível. Você para de brigar com o linter. Você para de cair em
closures obsoletas. Você começa a raciocinar como um engenheiro React sênior.
E o mais importante: você nunca mais vai suprimir o react-hooks/exhaustive-deps só para seguir
em frente.