Back

Por que o useEffect está rodando duas vezes? Guia Completo do Strict Mode no React 19

Você criou um projeto React novo, escreveu um useEffect simples pra buscar dados e... a requisição foi duas vezes. O console.log também saiu duplicado. Tudo rodou duas vezes.

"Isso é bug? Configurei algo errado? Preciso dar downgrade no React?"

Relaxa. Seu código não tá quebrado. É assim mesmo. Mais precisamente, o React faz isso de propósito. E entender por que esse comportamento existe vai te transformar em um desenvolvedor React significativamente melhor. Mais importante ainda, aprender a escrever Effects que funcionam corretamente nesse cenário vai prevenir bugs reais em produção que de outra forma passariam despercebidos.

Neste guia, vamos explorar exatamente o que tá acontecendo, por que o time do React tomou essa decisão aparentemente irritante, e o mais importante: como escrever Effects que são resistentes, corretos e prontos pra produção.


O que tá acontecendo?

Digamos que você escreveu isso:

import { useEffect, useState } from 'react'; function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { console.log('Buscando dados...', userId); fetch(`/api/users/${userId}`) .then(res => res.json()) .then(data => setUser(data)); }, [userId]); return <div>{user?.name}</div>; }

No modo desenvolvimento, o console mostra:

Buscando dados... 123
Buscando dados... 123

E na aba Network tem duas requisições idênticas pra /api/users/123. Estranho, né?

Se você já trabalha com React há um tempo, isso pode parecer esquisito. Antes do React 18, Effects rodavam uma vez só em desenvolvimento—igual à produção. O que mudou?


O culpado: React.StrictMode

A razão é o StrictMode. Normalmente tá assim no seu main.jsx ou index.js:

import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; createRoot(document.getElementById('root')).render( <StrictMode> <App /> </StrictMode> );

A partir do React 18, o StrictMode faz isso de propósito:

  1. Monta o componente
  2. Desmonta imediatamente
  3. Monta de novo

Isso só acontece em desenvolvimento. No build de produção, roda uma vez só.

O ponto chave: essa dupla invocação intencional foi projetada pra expor bugs no seu código.


Por que fazer isso de propósito? Entendendo as razões

O time do React não tá te trollando. Eles fizeram isso por uma verdade fundamental sobre Effects:

"Se quebra rodando duas vezes, também ia quebrar em produção. Você só não ia descobrir durante o desenvolvimento."

Pensa em cenários reais onde Effects podem rodar múltiplas vezes:

Cenário 1: Fast Refresh Durante Desenvolvimento

Você tá codando, salva um arquivo, e o Fast Refresh re-renderiza seu componente. Se seu Effect configura uma subscription mas não limpa corretamente, agora você tem subscriptions duplicadas.

Cenário 2: React Suspense e Transitions

Com features do React 18+ como startTransition e Suspense, o React pode precisar "suspender" um render, depois tentar de novo. Componentes podem montar, desmontar e remontar como parte do comportamento normal de rendering concorrente.

Cenário 3: Remontagem em Navegação Real

O usuário navega pra outra página, aperta o botão voltar, o componente remonta. Se seu Effect cria uma conexão WebSocket sem cleanup, agora você tem conexões órfãs.

Cenário 4: Features Futuras

O time do React tá preparando APIs "Offscreen" (renderizar componentes em background antes de serem visíveis). Essas vão exigir que componentes montem/desmontem múltiplas vezes como parte do ciclo de vida normal.

A dupla invocação no Strict Mode simula essas condições reais. Se seu código sobrevive esse teste de estresse, é muito mais provável que funcione corretamente em todos esses cenários.


O problema real: Effects sem cleanup

Vamos diagnosticar onde a maioria dos desenvolvedores erra:

useEffect(() => { const socket = new WebSocket('wss://api.example.com/realtime'); socket.onmessage = (event) => { setMessages(prev => [...prev, event.data]); }; }, []);

O que tá errado? Não tem função de cleanup. Quando o Strict Mode desmonta e remonta, você cria uma segunda conexão WebSocket. A primeira fica órfã—ainda recebendo mensagens, consumindo memória, mas sem referência pra fechar.

O padrão correto:

useEffect(() => { const socket = new WebSocket('wss://api.example.com/realtime'); socket.onmessage = (event) => { setMessages(prev => [...prev, event.data]); }; // 👇 Isso aqui é o pulo do gato. Chamado no unmount return () => { socket.close(); }; }, []);

Agora o fluxo é:

  1. Mount → cria WebSocket #1
  2. Strict Mode unmount → fecha WebSocket #1
  3. Remount → cria WebSocket #2

Sempre só uma conexão existe. Seu Effect é resistente.


Corrigindo padrões comuns de Effects

Vamos ver os padrões mais comuns e como fazer eles à prova do Strict Mode.

Padrão 1: Data fetching com AbortController

Abordagem problemática:

// ❌ Problemático: Duplo fetch, race conditions potenciais useEffect(() => { fetch(`/api/users/${userId}`) .then(res => res.json()) .then(setUser); }, [userId]);

Abordagem resistente:

// ✅ Correto: Aborta requisições em andamento no cleanup useEffect(() => { const controller = new AbortController(); fetch(`/api/users/${userId}`, { signal: controller.signal }) .then(res => res.json()) .then(setUser) .catch(err => { if (err.name !== 'AbortError') { console.error('Deu ruim:', err); } }); return () => controller.abort(); }, [userId]);

O que isso resolve:

  • Quando Strict Mode desmonta, o cleanup aborta a primeira requisição
  • A segunda requisição prossegue normalmente
  • Em navegação rápida, requisições antigas não sobrescrevem dados novos (problema de race condition)

Padrão 2: Event listeners no window/document

// ❌ Problemático: Listeners vão acumulando useEffect(() => { window.addEventListener('resize', handleResize); }, []);
// ✅ Correto: Remove direitinho useEffect(() => { window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []);

Padrão 3: Timers e intervals

// ❌ Problemático: Múltiplos intervals rodando useEffect(() => { setInterval(() => { setCount(c => c + 1); }, 1000); }, []);
// ✅ Correto: Limpa o interval useEffect(() => { const id = setInterval(() => { setCount(c => c + 1); }, 1000); return () => clearInterval(id); }, []);

Padrão 4: Inicialização de bibliotecas de terceiros

Bibliotecas de gráficos, frameworks de animação ou ferramentas de manipulação DOM precisam de inicialização e destruição:

// ❌ Problemático: Chart inicializa duas vezes useEffect(() => { const chart = new ChartLibrary(containerRef.current, { data: chartData, }); }, [chartData]);
// ✅ Correto: Destroy no cleanup useEffect(() => { const chart = new ChartLibrary(containerRef.current, { data: chartData, }); return () => chart.destroy(); }, [chartData]);

Padrão 5: Analytics e tracking

Isso é um pouco sutil. Você provavelmente não quer deduplicar seu analytics:

// Dispara duas vezes no Strict Mode - é problema? useEffect(() => { analytics.track('page_viewed', { page: pathname }); }, [pathname]);

A resposta: Depende do seu serviço de analytics. A maioria dos analytics modernos (GA4, Amplitude, Mixpanel) são projetados pra lidar com eventos duplicados ou fazer debounce no servidor. O disparo duplo em desenvolvimento não deveria causar problemas nos dados de produção.

Se realmente importa pro seu caso, você pode usar um ref pra rastrear se já fez o tracking:

const hasTracked = useRef(false); useEffect(() => { if (!hasTracked.current) { analytics.track('page_viewed', { page: pathname }); hasTracked.current = true; } }, [pathname]);

Mas cuidado: esse padrão pode esconder bugs legítimos. Use com moderação.


O anti-padrão da flag booleana

Quando encontra o Strict Mode pela primeira vez, alguns devs tentam essa "solução":

// ⚠️ Anti-padrão: Não faça isso useEffect(() => { let ignore = false; const run = async () => { const data = await fetchData(); if (!ignore) { setData(data); } }; run(); return () => { ignore = true }; }, []);

Peraí—esse padrão é na verdade correto pra data fetching assíncrono! A flag ignore previne setar state depois do unmount. Mas às vezes os devs aplicam errado:

// ❌ Errado: Isso anula o propósito do Strict Mode const hasRun = useRef(false); useEffect(() => { if (hasRun.current) return; hasRun.current = true; // lógica do Effect... }, []);

Esse padrão diz "só roda uma vez, mesmo no Strict Mode." Por que é problemático?

  1. Esconde bugs. Se seu Effect precisa de cleanup mas não tem, esse padrão mascara o problema durante o desenvolvimento.
  2. Quebra em cenários de produção. E se seu componente legitimamente remonta? O Effect não vai rodar de novo.
  3. Viola as expectativas do React. Effects devem ser resistentes a múltiplas invocações por design.

Conclusão: Se você tá buscando padrões de "rodar só uma vez", se pergunte: por que meu Effect quebra quando roda duas vezes? A resposta geralmente revela uma função de cleanup que você esqueceu de escrever.


Novos padrões do React 19: Server Actions e o hook use

O React 19 introduz padrões que evitam completamente algumas dessas armadilhas de Effects. Conhecer eles pode modernizar sua abordagem de data fetching.

O hook use pra buscar dados

O React 19 introduz o hook use pra consumir promises e context:

import { use, Suspense } from 'react'; function UserProfile({ userPromise }) { // `use` desembrulha a promise const user = use(userPromise); return <div>{user.name}</div>; } // Componente pai function App({ userId }) { const [userPromise] = useState(() => fetchUser(userId)); return ( <Suspense fallback={<div>Carregando...</div>}> <UserProfile userPromise={userPromise} /> </Suspense> ); }

Por que importa: A promise é criada uma vez no pai e passada pra baixo. O componente filho não gerencia o ciclo de vida do fetching—ele só lê o resultado. Isso evita completamente o problema do "duplo fetch" porque não tem Effect envolvido.

Server Actions pra mutações

Pra mutações de dados, os Server Actions do React 19 oferecem um modelo diferente:

// actions.js - roda no servidor 'use server'; export async function createUser(formData) { const user = await db.users.create({ name: formData.get('name'), email: formData.get('email'), }); return user; }
// Componente - sem useEffect necessário pra submit import { createUser } from './actions'; function CreateUserForm() { return ( <form action={createUser}> <input name="name" /> <input name="email" /> <button type="submit">Criar</button> </form> ); }

Server Actions são invocados como form actions, eliminando a necessidade de handlers de submit e Effects. A action roda no servidor, e o React cuida de atualizar a UI.

Quando você ainda precisa de useEffect

Esses novos padrões são poderosos, mas useEffect não vai sumir. Você ainda precisa dele pra:

  • Medições de DOM (ler dimensões de elementos)
  • Subscriptions de APIs do navegador (IntersectionObserver, ResizeObserver)
  • Integração com bibliotecas de terceiros
  • Animações disparadas por mudanças de state
  • Conexões WebSocket
  • Gestão de timers/intervals

Pra esses casos, o padrão de função de cleanup continua essencial.


Estratégia de debug: É Strict Mode ou bug real?

Quando você vir duplas invocações, aqui vai uma abordagem sistemática de debug:

Passo 1: Confirma que o Strict Mode tá ativo

Verifica seu entry point (index.js, main.jsx, main.tsx):

<StrictMode> <App /> </StrictMode>

Se StrictMode não tá lá, as duplas invocações indicam um bug real—provavelmente na sua lógica de roteamento ou componente pai.

Passo 2: Adiciona logging pra entender o fluxo

useEffect(() => { console.log('Effect SETUP pra:', userId); return () => { console.log('Effect CLEANUP pra:', userId); }; }, [userId]);

No Strict Mode você vai ver:

Effect SETUP pra: 123
Effect CLEANUP pra: 123
Effect SETUP pra: 123

Isso confirma o ciclo mount → unmount → remount.

Passo 3: Testa no modo produção

Roda um build de produção localmente:

npm run build npm run preview

Em produção, o Strict Mode tá desabilitado. Se a dupla invocação persistir, você tem um bug real.

Passo 4: Verifica seu array de dependências

Uma causa comum de re-execuções inesperadas são dependências instáveis:

// ❌ Cria novo objeto todo render → Effect roda todo render useEffect(() => { // ... }, [{ some: 'object' }]); // ✅ Primitivo estável useEffect(() => { // ... }, [userId]);

Objetos, arrays e funções definidos inline durante o render são novos em cada render, fazendo os Effects re-executarem.


Implicações de performance: Devo me preocupar?

Uma preocupação natural: "Essa execução dupla não vai deixar meu app lento?"

Em desenvolvimento: Sim, marginalmente. Mas a velocidade de desenvolvimento é inerentemente diferente de produção. Os benefícios de segurança superam os milissegundos.

Em produção: O Strict Mode é completamente removido. Zero impacto na performance. Seu Effect roda exatamente uma vez por mount/mudança de dependência.

O time do React sempre lembra: não otimize pra performance de desenvolvimento. O build de dev inclui muitos checks, warnings e slowdowns intencionais que não existem em produção.


Race conditions: Cenários intermediários

Considera um usuário trocando rapidamente entre perfis:

// Usuário clica rápido: Perfil A → Perfil B → Perfil C

Sem cleanup adequado, você pode ver:

  1. Requisição A começa
  2. Requisição B começa
  3. Requisição C começa
  4. Requisição A completa → state vira User A
  5. Requisição C completa → state vira User C
  6. Requisição B completa → state vira User B ← Errado!

O usuário espera ver User C, mas User B foi o último a completar.

AbortController resolve isso: quando o usuário clica no Perfil B, Requisição A é abortada. Quando clica no Perfil C, Requisição B é abortada. Só Requisição C completa.


Effects que devem rodar só no mount

Às vezes você genuinamente precisa de lógica que só roda no mount e não re-executa. Use um ref com cuidado:

const initialized = useRef(false); useEffect(() => { // Roda setup que só deve acontecer uma vez initializeComplexLibrary(); if (!initialized.current) { initialized.current = true; // Setup único como registrar o app com um backend registerAppInstance(); } return () => { // Mas sempre faz cleanup cleanupComplexLibrary(); }; }, []);

Nota: o cleanup ainda roda toda vez. O ref só protege a lógica de registro "única".


Sincronizando com sistemas externos

Quando seu Effect sincroniza state com algo fora do React (DOM, API externa, storage do navegador):

useEffect(() => { // Lê state externo no mount const savedTheme = localStorage.getItem('theme'); if (savedTheme) setTheme(savedTheme); // Não precisa de cleanup pra leitura // Mas se você configura listeners, limpa eles const handler = (e) => { if (e.key === 'theme') setTheme(e.newValue); }; window.addEventListener('storage', handler); return () => window.removeEventListener('storage', handler); }, []);

Melhores práticas pra Effects resistentes

Vamos consolidar tudo em um conjunto de boas práticas acionáveis:

1. Sempre retorna uma função de cleanup

Faça disso um hábito. Mesmo se você acha que não precisa de cleanup:

useEffect(() => { // Lógica de setup return () => { // Lógica de cleanup (mesmo se for um comentário vazio) // Isso te força a pensar no que precisa ser limpo }; }, [deps]);

2. Usa AbortController pra todas as chamadas fetch

Esse padrão deveria ser natural:

useEffect(() => { const controller = new AbortController(); fetch(url, { signal: controller.signal }) .then(/* ... */) .catch(err => { if (err.name !== 'AbortError') throw err; }); return () => controller.abort(); }, [url]);

3. Armazena referências mutáveis em refs

Quando Effects precisam de state mutável que persiste através de ciclos de cleanup:

const socketRef = useRef(null); useEffect(() => { socketRef.current = new WebSocket(url); return () => socketRef.current?.close(); }, [url]);

4. Extrai custom hooks pra padrões reutilizáveis

Centraliza o comportamento correto:

function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); setLoading(true); fetch(url, { signal: controller.signal }) .then(res => { if (!res.ok) throw new Error(res.statusText); return res.json(); }) .then(setData) .catch(err => { if (err.name !== 'AbortError') setError(err); }) .finally(() => setLoading(false)); return () => controller.abort(); }, [url]); return { data, loading, error }; }

5. Considera React Query ou SWR

Pra data fetching complexo, essas bibliotecas lidam com caching, deduplicação e cleanup automaticamente:

// Usando React Query import { useQuery } from '@tanstack/react-query'; function UserProfile({ userId }) { const { data: user, isLoading, error } = useQuery({ queryKey: ['user', userId], queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()), }); if (isLoading) return <div>Carregando...</div>; if (error) return <div>Erro aconteceu</div>; return <div>{user.name}</div>; }

O React Query deduplica requisições automaticamente—mesmo com o mount duplo do Strict Mode, você vai ver só uma requisição de rede.


Desabilitar Strict Mode: deveria?

Você pode remover o StrictMode:

// Antes <StrictMode> <App /> </StrictMode> // Depois <App />

Deveria? Quase nunca. Motivos:

  1. Você tá escondendo bugs, não consertando. Esses bugs vão aparecer em produção.
  2. Você perde preparação pro futuro. Próximas features do React podem depender de Effects resistentes.
  3. É code smell. Se você precisa desabilitar o Strict Mode, seus Effects provavelmente têm problemas estruturais.

A única razão legítima pra desabilitar temporariamente o Strict Mode é quando você tá debugando pra isolar se um problema é relacionado ao Strict Mode ou um bug separado.


Conclusão: Abraça a execução dupla

O Strict Mode do React 19 não tá trabalhando contra você—tá te treinando pra escrever código melhor. Toda vez que você encontrar uma execução dupla, se pergunte:

  1. Meu Effect tem uma função de cleanup?
  2. Minha função de cleanup realmente limpa o que o Effect configurou?
  3. Meu Effect funcionaria corretamente se rodasse 3 vezes? 10 vezes? 100 vezes?

Se a resposta pras três é "sim", seu Effect tá pronto pra produção.

Os padrões neste guia—AbortController pra fetches, funções de cleanup pra subscriptions, refs pra state mutável—não são workarounds do Strict Mode. São a forma correta de escrever Effects. O Strict Mode só faz a importância deles inegável.

Da próxima vez que seu Effect rodar duas vezes, não procura workaround. Agradece ao Strict Mode por pegar um bug antes dos seus usuários, e escreve uma função de cleanup. Seu eu do futuro—debugando um problema de produção às 2 da manhã—vai agradecer.


Última atualização: Dezembro 2025

reactreact-19useeffectstrict-modedebuggingweb-development