Pular para o conteúdo

react, hooks, useEffect14 min de leitura

O Modelo Mental Definitivo do useEffect — Chega de Bugs, Chega de Adivinhação

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 useEffect até 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:

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:

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

.ts
useEffect(setup, deps?)

Ele retorna undefined e aceita dois argumentos:

  1. 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).
  2. 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
OmitidoRoda 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.

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

Um componente que recria um objeto de configurações e uma função logSuccess a cada renderização, e os lista como dependências do useEffect
Ruim: o objeto e a função são recriados a cada renderização, então o linter aponta os dois como dependências instáveis.

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

Aviso do ESLint react-hooks/exhaustive-deps: o objeto DEFAULT_SETTINGS faz as dependências do Effect mudarem a cada renderização
O react-hooks/exhaustive-deps aponta o objeto: sua identidade muda a cada renderização.
Aviso do ESLint react-hooks/exhaustive-deps: a função logSuccess faz as dependências do Effect mudarem a cada renderização
A mesma regra aponta a função, pelo mesmo motivo.

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.

O objeto de configurações e a função logSuccess movidos para fora do componente, agora tratados como não reativos pelo linter
Bom: definidos uma única vez, fora do componente, eles são não reativos por construção — sem aviso, sem re-execuções desnecessárias.

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.

.tsx
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:

  • useMemo para objetos, arrays ou valores calculados.
  • useCallback para 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.

.tsx
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 useEffectEvent para 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 useEffectEvent só 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.

.tsx
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 fetch dentro 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.