Por que seu useEffect executa duas vezes
Entenda por que o React StrictMode executa useEffect duas vezes no desenvolvimento e corrija buscas, ouvintes e outros efeitos com a limpeza adequada.
Em desenvolvimento, o StrictMode do React executa o setup de cada efeito, depois o cleanup e, em seguida, o setup novamente. Por isso, qualquer efeito que faça uma requisição, crie uma inscrição (subscription) ou registre logs vai acontecer duas vezes de forma visível.
Você escreve um único fetch em um efeito, abre a aba Network e encontra duas requisições idênticas e dois logs no console. Parece um bug no seu código ou no React.
Não é nenhum dos dois. A execução dupla é um teste proposital da sua lógica de cleanup. Este artigo explica o que o StrictMode está fazendo, como corrigir os três efeitos mais comuns que falham nesse teste, quais “correções” apenas escondem o problema e como saber que você terminou. Se precisar revisar o hook em si, comece por este guia sobre o hook useEffect do React.
Principais conclusões
- O StrictMode executa um ciclo extra de setup e cleanup para cada efeito, apenas em desenvolvimento, tanto no React 18 quanto no React 19.
- O StrictMode é opcional (opt-in), mas o template React do Vite envolve a aplicação em
<StrictMode>, e o App Router do Next.js o ativa por padrão. - Uma função de cleanup correta desfaz exatamente o que o setup fez: aborta a requisição, remove o listener, limpa o timer ou fecha a conexão.
- Duas requisições na aba Network durante o desenvolvimento são esperadas mesmo depois de uma correção adequada. O que importa é que apenas a resposta mais recente possa atualizar o estado.
- Remover o StrictMode ou adicionar uma guarda de “já executou” com
useRefesconde a ausência de cleanup sem corrigi-la.
O sintoma: um fetch que dispara duas vezes
Um efeito de busca de dados sem cleanup é a forma mais comum de as pessoas descobrirem esse comportamento. Este componente funciona bem em produção e dispara duas vezes em desenvolvimento:
import { useState, useEffect } from 'react';
function ProductDetails({ id }) {
const [product, setProduct] = useState(null);
useEffect(() => {
console.log('setup');
fetch(`/api/products/${id}`)
.then((res) => res.json())
.then((data) => setProduct(data));
}, [id]);
return <h2>{product?.name}</h2>;
}
Você recebe dois logs setup e duas requisições. Nada informa ao React como cancelar a primeira requisição, então ambas são executadas até o fim.
Por que o useEffect executa duas vezes em desenvolvimento?
A referência do StrictMode do React explica que, com o StrictMode ativado, o React adiciona uma passagem extra de setup e cleanup a cada efeito durante o desenvolvimento. Seu efeito executa o setup, depois o cleanup e, então, o setup novamente, como se o componente fosse montado, desmontado e montado outra vez imediatamente.
A execução dupla acontece apenas em builds de desenvolvimento. Em produção, um efeito é executado uma vez quando o componente é montado e novamente apenas quando suas dependências mudam ou quando o componente é remontado.
O StrictMode é opcional. Ele se aplica apenas aos componentes envolvidos em <StrictMode>. Muitos projetos, porém, já vêm com esse wrapper por padrão. O main.jsx do template React do Vite renderiza <App /> dentro de <StrictMode>. O App Router do Next.js ativa o StrictMode por padrão desde o Next.js 13.5.1. Aplicações com Pages Router precisam ativá-lo explicitamente com reactStrictMode: true.
Esse ciclo verifica se o seu cleanup realmente funciona. Se um efeito se comporta mal quando o React executa setup, cleanup e setup, ele também vai se comportar mal quando um usuário sair de uma página e voltar. Também vai se comportar mal quando edições no código o reexecutarem em desenvolvimento: a documentação de Fast Refresh do Next.js diz que os efeitos devem tolerar reexecuções ocasionais e observa que o StrictMode reforça essa exigência. Para entender em detalhes quando o cleanup é disparado em relação à pintura (paint) da tela, veja useEffect vs useLayoutEffect.
Como corrigir um useEffect que executa duas vezes?
Para corrigir um useEffect que executa duas vezes, forneça a ele uma função de cleanup que reverta o que o setup iniciou. Cada falha comum tem uma correção específica:
| O que você vê em dev | O que está realmente errado | Correção |
|---|---|---|
| Dois fetches, possíveis dados desatualizados | Nada cancela a requisição em andamento | AbortController ou uma flag ignore no cleanup |
| Um listener ou uma inscrição dispara duas vezes | Ele nunca é removido | Remova-o no cleanup |
| Um contador termina em 2 | A lógica não é um efeito colateral | Mova-a para um event handler |
Um fetch que dispara duas vezes
Um fetch que dispara duas vezes no useEffect tem duas correções possíveis. A primeira é abortar a requisição no cleanup. Quando um fetch é abortado, ele é rejeitado com uma DOMException AbortError, que você pode ignorar com segurança:
useEffect(() => {
const controller = new AbortController();
fetch(`/api/products/${id}`, { signal: controller.signal })
.then((res) => res.json())
.then((data) => setProduct(data))
.catch((err) => {
if (err.name !== 'AbortError') throw err;
});
return () => controller.abort();
}, [id]);
A outra opção é a flag ignore, que é o padrão usado pelo guia do React sobre sincronização com efeitos para busca de dados:
useEffect(() => {
let ignore = false;
fetch(`/api/products/${id}`)
.then((res) => res.json())
.then((data) => {
if (!ignore) setProduct(data);
});
return () => {
ignore = true;
};
}, [id]);
Você continuará vendo duas requisições em desenvolvimento depois de qualquer uma das correções. Com AbortController, a primeira requisição é abortada. Com ignore, ambas as requisições são concluídas, mas apenas a segunda pode atualizar o estado. Em produção, apenas uma requisição é enviada. O mesmo cleanup também impede que uma resposta lenta sobrescreva uma mais recente quando id muda.
Um listener que vaza
Um event listener adicionado no useEffect vaza, a menos que o cleanup o remova. Declare o handler dentro do efeito para que o cleanup remova a mesma referência de função que o setup adicionou:
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
Um contador que incrementa duas vezes
// Before: ends at 2 in development
useEffect(() => {
setCount((c) => c + 1);
}, []);
O setup executa duas vezes em desenvolvimento, então o incremento também executa duas vezes. Nenhum cleanup consegue “desfazer o incremento” do contador, e isso indica que esse efeito nem deveria existir. Coloque o incremento onde o evento que o dispara acontece:
<button onClick={() => setCount((c) => c + 1)}>Add</button>
Você deve remover o StrictMode ou adicionar uma guarda com useRef?
Não. As duas abordagens fazem a execução dupla desaparecer e mantêm o bug de fundo.
Desativar o StrictMode, seja removendo o wrapper ou definindo reactStrictMode: false na configuração do Next.js, elimina o teste. Os efeitos continuam sem cleanup.
A guarda com useRef é a opção mais tentadora:
// Avoid: hides missing cleanup
const hasRun = useRef(false);
useEffect(() => {
if (hasRun.current) return;
hasRun.current = true;
subscribe();
}, []);
A guarda pula o segundo setup em desenvolvimento, e o console parece limpo. Uma flag com useRef que pula a segunda execução não corrige a ausência de cleanup. Ela esconde o problema em desenvolvimento e mantém o vazamento em toda remontagem real. Uma ref pertence a uma única instância do componente. Quando uma rota é desmontada e montada novamente em produção, a nova instância recebe uma nova ref, então o efeito executa de novo. Continua não havendo cleanup, portanto a inscrição antiga nunca é removida.
Em um session replay de uma aplicação com cleanup de efeitos ausente, esse bug pode aparecer como requisições duplicadas ou dados desatualizados depois de navegar com os botões de voltar e avançar. É o mesmo bug que o StrictMode revela em desenvolvimento.
Quando você não precisa de um efeito
Muitos efeitos que disparam duas vezes nunca deveriam ter sido efeitos. Efeitos servem para sincronizar com algo externo ao React pelo fato de o componente estar na tela. Você talvez não precise de um efeito aborda os dois maiores grupos de efeitos desnecessários.
Trabalho orientado a eventos pertence aos event handlers. Enviar um formulário, exibir um toast depois de clicar em “Adicionar ao carrinho” ou incrementar um contador acontece porque o usuário fez algo, e não porque um componente foi renderizado.
Valores que podem ser calculados a partir de props ou do estado pertencem à renderização:
// Before: extra state, extra render, effect runs twice
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(first + ' ' + last);
}, [first, last]);
// After: computed during render
const fullName = first + ' ' + last;
Uma verificação rápida: seu efeito está correto?
Para verificar se um useEffect está correto, registre logs tanto no setup quanto no cleanup:
useEffect(() => {
console.log('setup');
return () => console.log('cleanup');
}, []);
Em desenvolvimento, o console deve exibir:
setup
cleanup
setup
Um build de produção registra setup apenas uma vez. Se os seus logs mostram setup, cleanup, setup e a interface termina no estado correto, o efeito está funcionando como esperado e não há nada a corrigir.
Conclusão
Um useEffect que executa duas vezes em desenvolvimento é o StrictMode verificando se o seu cleanup funciona. Não o silencie. Revise cada efeito que dispara duas vezes e faça com que ele passe na verificação: aborte ou ignore requisições desatualizadas, remova o que você adiciona e tire completamente dos efeitos a lógica orientada a eventos e os valores derivados. Depois que seus efeitos estiverem fazendo o cleanup corretamente, adicione error boundaries do React para que um componente que lance um erro durante a renderização não derrube a página inteira.
Perguntas frequentes
Por que meu useEffect executa duas vezes em produção, onde o StrictMode está desativado?
Em produção, um efeito executa novamente quando o valor de uma de suas dependências muda ou quando o componente é remontado. Se qualquer dependência for diferente da última renderização, o efeito executa de novo; por isso, objetos e funções criados durante a renderização contam como valores novos todas as vezes. Alterar a prop key, ou uma renderização condicional que remove e adiciona novamente o componente, também o remonta e executa o setup outra vez.
Devo impedir que um evento de analytics dispare duas vezes em desenvolvimento?
Não. A documentação do React recomenda manter uma chamada de analytics de visita à página dentro do seu efeito. Os usuários não conseguem perceber se ela executou uma ou duas vezes, e um build de produção envia cada visita apenas uma vez. Para começar, sua máquina de desenvolvimento não deveria estar enviando eventos para as métricas de produção. Se precisar depurar os eventos, teste em um build de staging executado em modo de produção ou desative o StrictMode temporariamente.
O useLayoutEffect também executa duas vezes com o StrictMode?
Sim. A referência do React para useLayoutEffect descreve o mesmo comportamento em desenvolvimento que o do useEffect: com o StrictMode ativado, o React faz primeiro uma passagem de setup e cleanup e, depois, o setup real. Layout effects precisam do mesmo cleanup correspondente, como desconectar um observer ou destruir a instância de um widget de terceiros. Trocar para useLayoutEffect altera o momento em que o efeito executa em relação à pintura da tela, e não quantas vezes ele executa em desenvolvimento.
Posso desativar o StrictMode para um componente e mantê-lo no restante da aplicação?
Não. Quando uma árvore é envolvida pelo StrictMode, todos os componentes dentro dela recebem as verificações, e nenhum componente individual pode ficar de fora. O que você pode fazer é mover o wrapper do StrictMode para um nível mais baixo da árvore, de modo que ele cubra apenas algumas partes da aplicação. Se o StrictMode não envolver a raiz, o React deixa de fazer a execução extra dos efeitos na primeira montagem. Essa execução faria os efeitos dos filhos dispararem duas vezes enquanto os efeitos dos pais disparariam uma vez, algo que não pode acontecer em produção.
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 GitHub12kObservação: a tradução foi feita em português do Brasil, a variante mais comum em conteúdo técnico sobre React. Os comentários dentro dos blocos de código foram mantidos em inglês, conforme a instrução de não alterar os trechos de código. Se preferir, posso adaptar o texto para o português europeu ou traduzir também os comentários.