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:
- Monta o componente
- Desmonta imediatamente
- 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 é:
- Mount → cria WebSocket #1
- Strict Mode unmount → fecha WebSocket #1
- 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?
- Esconde bugs. Se seu Effect precisa de cleanup mas não tem, esse padrão mascara o problema durante o desenvolvimento.
- Quebra em cenários de produção. E se seu componente legitimamente remonta? O Effect não vai rodar de novo.
- 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:
- Requisição A começa
- Requisição B começa
- Requisição C começa
- Requisição A completa → state vira User A
- Requisição C completa → state vira User C
- 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:
- Você tá escondendo bugs, não consertando. Esses bugs vão aparecer em produção.
- Você perde preparação pro futuro. Próximas features do React podem depender de Effects resistentes.
- É 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:
- Meu Effect tem uma função de cleanup?
- Minha função de cleanup realmente limpa o que o Effect configurou?
- 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