Os exemplos de código requerem React 18+. O comportamento de invocação dupla StrictMode foi introduzido no React 18.
Funções de limpeza perdidas no React useEffect estão entre as fontes mais comuns de vazamentos de memória, bugs de estado obsoleto e condições de corrida. Tutoriais típicos para iniciantes demonstram o caminho feliz: buscar dados, definir estado, renderizar. Eles raramente abordam o que acontece quando um componente é desmontado no meio do voo, quando uma dependência muda mais rápido do que uma operação assíncrona é resolvida ou quando os ouvintes de eventos se acumulam silenciosamente nas rerenderizações. Este artigo fornece um modelo mental unificado para limpeza de useEffect em três categorias (temporizadores, ouvintes e corridas de busca), uma configuração de mapeamento de tabela de referência para limpeza até a armadilha e uma lista de verificação de revisão de PR copiar e colar que as equipes de desenvolvimento podem adotar imediatamente.
Índice
Como o useEffect Cleanup realmente funciona
O ciclo de vida de configuração-limpeza em uma frase
O modelo mental é simples: o React executa a função de configuração quando o componente é montado, executa a função de limpeza quando o componente é desmontado e, em cada mudança de dependência, executa a limpeza para o efeito da renderização anterior antes de executar a configuração para o efeito da renderização atual. Esse ciclo de limpeza e configuração na mudança de dependência é a parte que a maioria dos desenvolvedores ignora ou entende mal. Isso significa que cada invocação de efeito é combinada com uma desmontagem correspondente, e o React garante essa ordem. A função de configuração adquire recursos; o retorno da limpeza os libera. A matriz de dependência declara quais valores, quando alterados, acionam esse ciclo novamente.
Por que StrictMode invoca seus efeitos duas vezes
O StrictMode do React 18 executa intencionalmente o efeito de cada componente duas vezes em desenvolvimento simulando uma desmontagem e remontagem: setup → cleanup → setup. Sua função de configuração é executada duas vezes na renderização inicial em desenvolvimento. Essa invocação dupla existe especificamente para revelar a lógica de limpeza ausente. Se um componente registrar um ouvinte de evento na configuração, mas nunca o remover na limpeza, o StrictMode produzirá dois ouvintes após a remontagem simulada, tornando o bug imediatamente visível durante o desenvolvimento. Conforme documentado na documentação do React no StrictMode (https://react.dev/reference/react/StrictMode), esse comportamento ocorre apenas no modo de desenvolvimento e não afeta compilações de produção. No entanto, os bugs que expõe são inteiramente reais. Um componente que se comporta incorretamente na montagem dupla do StrictMode tem um bug de limpeza latente que se manifestará na produção sempre que o componente for remontado devido à navegação, renderização condicional ou nova renderização pai.
Um componente que se comporta incorretamente na montagem dupla do StrictMode tem um bug de limpeza latente que se manifestará na produção sempre que o componente for remontado devido à navegação, renderização condicional ou nova renderização pai.
import { useEffect } from 'react';
function ExampleComponent({ dependency }) {
useEffect(() => {
console.log('Effect setup with dependency:', dependency);
return () => {
console.log('Effect cleanup for dependency:', dependency);
};
}, (dependency));
return <div>Anatomy of useEffectdiv>;
}
Padrão 1: Limpando temporizadores (setInterval / setTimeout)
O bug: intervalos de empilhamento na nova renderização
Quando um setInterval começa dentro de um useEffect que depende de um valor de prop ou estado, toda vez que a dependência muda, o React executa novamente o efeito. Sem limpeza, o intervalo anterior continua em execução. Um segundo intervalo começa. Depois um terceiro. O sintoma observável depende do que o intervalo faz: um contador aumenta N vezes por tick (onde N é o número de intervalos órfãos), um retorno de chamada de pesquisa é acionado várias vezes por tick ou uma animação da UI acelera de forma irregular. Cada intervalo órfão mantém um fechamento sobre o estado obsoleto, agravando o problema. Na montagem dupla do StrictMode em desenvolvimento, esse bug surge imediatamente como dois intervalos em execução desde a primeira renderização.
A correção: clearInterval no retorno de limpeza
O padrão correto cria o intervalo na configuração e o limpa na limpeza. Quando a dependência muda, o React limpa o intervalo antigo antes de iniciar um novo com o valor atualizado. Isto garante exatamente um intervalo ativo em qualquer momento.
import { useEffect, useState } from 'react';
function PollingComponent({ delay }) {
const (count, setCount) = useState(0);
useEffect(() => {
if (!delay || delay <= 0 || !Number.isFinite(delay)) return;
const id = setInterval(() => {
setCount(c => c + 1);
}, delay);
return () => {
clearInterval(id);
};
}, (delay));
return <div>Count: {count}div>;
}
O mesmo princípio se aplica a setTimeout. Substituir setInterval com setTimeout e clearInterval com clearTimeout. Armazene o ID do timer retornado pela função de agendamento e limpe-o no retorno de limpeza.
Padrão 2: Limpando Ouvintes de Eventos
O bug: acúmulo de ouvintes na janela/documento
Adicionando um ouvinte de evento a window, documentou qualquer nó DOM fora da própria árvore de renderização do componente requer limpeza manual. Sem uma correspondência removeEventListener no retorno de limpeza, cada nova renderização (ou montagem dupla StrictMode) anexa um manipulador adicional. Os manipuladores se acumulam. Um ouvinte de redimensionamento que atualiza o estado acionará setState várias vezes por evento de redimensionamento, uma vez para cada ouvinte órfão. Isso degrada o desempenho e pode causar corrupção sutil de estado quando vários manipuladores correm para atualizar a mesma variável de estado.
A correção: combinar adição e remoção com a mesma referência
O detalhe crítico é que removeEventListener deve receber exatamente a mesma referência de função passada para addEventListener. Uma função de seta embutida criada dentro do corpo do efeito produz uma nova referência em cada invocação, portanto, chamar removeEventListener com uma seta embutida diferente não faz nada. O ouvinte nunca é removido. Declare o manipulador como uma função nomeada (ou uma variável que contém a função) dentro do efeito e, em seguida, passe a mesma variável para ambos addEventListener e removeEventListener.
O detalhe crítico é que
removeEventListenerdeve receber exatamente a mesma referência de função passada paraaddEventListener.
import { useEffect, useState } from 'react';
function WindowSizeTracker() {
const (width, setWidth) = useState(typeof window !== 'undefined' ? window.innerWidth : 0);
useEffect(() => {
if (typeof window === 'undefined') return;
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, ());
return <div>Window width: {width}pxdiv>;
}
Se o manipulador depender de um valor prop ou state e for definido fora do efeito (por exemplo, com useCallback), esse manipulador deverá aparecer na matriz de dependência. Quando a referência muda, o React limpa o ouvinte antigo e anexa o novo. Observe que adicionar novamente o ouvinte em cada alteração de dependência é um comportamento correto: o manipulador antigo (com fechamento obsoleto) é removido e o novo manipulador (com valores novos) é anexado.
Padrão 3: Cancelando condições de corrida de busca com AbortController
O bug: respostas obsoletas substituindo o estado novo
Considere um componente de pesquisa onde o usuário digita rapidamente. Cada pressionamento de tecla aciona uma nova busca. A solicitação de “rea” pode levar 300ms enquanto a solicitação de “react” leva 50ms. A resposta “react” chega primeiro e a UI mostra os resultados corretos. Em seguida, a resposta “rea” chega e, como nada a impediu de chamar setState, ela substitui a interface do usuário com resultados obsoletos e incorretos. Esta é uma condição de corrida clássica. A resposta mais lenta vence não porque seja mais recente, mas porque é resolvida mais tarde. Sem cancelamento, não há mecanismo que diga ao navegador para descartar uma solicitação em andamento cujos resultados não são mais relevantes.
A correção: sinal AbortController na limpeza
A solução moderna é AbortController. Cada invocação de efeito cria uma nova instância de controlador. O controlador signal propriedade é passada para fetch como uma opção. Quando a função de limpeza é executada (porque a dependência foi alterada ou o componente foi desmontado), ela chama controller.abort()que cancela a solicitação em andamento. A promessa de busca é rejeitada com um AbortError. Os navegadores geralmente também tentam cancelar a solicitação de rede subjacente, embora o comportamento varie de acordo com o mecanismo (consulte o Buscar especificações no AbortSignal e tabelas de compatibilidade de navegador do MDN). Isso garante que apenas a solicitação mais recente possa definir o estado com êxito.
Lidando com o AbortError normalmente
Chamando abort() rejeita a promessa. Isso é normal, não uma condição de erro. O bloco catch deve distinguir AbortError de falhas de rede genuínas. Sem essa proteção, a UI exibiria um erro para cada cancelamento intencional ou a rejeição acionaria a infraestrutura de registro de erros. A verificação padrão inspeciona o erro name propriedade: se for igual 'AbortError'o bloco catch retorna mais cedo sem atualizar o estado ou revelar um erro.
import { useEffect, useState } from 'react';
function SearchResults({ query }) {
const (results, setResults) = useState(());
const (error, setError) = useState(null);
const (loading, setLoading) = useState(false);
useEffect(() => {
if (!query) {
setResults(());
setError(null);
setLoading(false);
return;
}
const controller = new AbortController();
async function fetchResults() {
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
const items = Array.isArray(data?.items) ? data.items : ();
setResults(items);
} catch (err) {
if (err.name === 'AbortError') {
return;
}
setError(err.message);
} finally {
setLoading(false);
}
}
fetchResults();
return () => {
controller.abort();
};
}, (query));
if (error) return <div>Error: {error}div>;
if (loading) return <div>Loading...div>;
return (
<ul>
{results.map((item, index) => (
<li key={item.id ?? `item-${index}`}>{item.title ?? '(Untitled)'}li>
))}
ul>
);
}
Observe que a função de efeito em si não é assíncrona. Uma função assíncrona interna é declarada e chamada imediatamente. Isso é importante porque useEffect espera que seu valor de retorno seja indefinido ou uma função de limpeza. O React ignora uma promessa retornada e nunca registra uma função de limpeza dela.
Tabela de referência: Visão geral da configuração, limpeza e armadilhas
A tabela a seguir captura o padrão universal dos efeitos colaterais mais comuns em aplicativos React. Cada linha mostra a mesma estrutura: adquirir um recurso na configuração, liberá-lo na limpeza e compreender o modo de falha específico quando a limpeza estiver faltando.
| Cenário | Ação de configuração | Ação de limpeza | Armadilha Comum |
|---|---|---|---|
setInterval / setTimeout | Iniciar cronômetro | clearInterval / clearTimeout | Intervalos de empilhamento na mudança de dependência |
addEventListener | Anexar manipulador | removeEventListener com a mesma referência | Seta embutida ≠ mesma referência |
fetch | Solicitação de fogo com signal | controller.abort() | A resposta obsoleta substitui o estado |
| WebSocket | new WebSocket(url) + anexar manipuladores | Manipuladores nulos, então ws.close() | Vazamento de conexão ao desmontar |
Assinatura de terceiros (por exemplo, Firebase onSnapshot) | Inscrever-se | Chamar função de cancelamento de assinatura | Esquecer o valor de retorno é o suspeito |
Os padrões WebSocket e Firebase aparecem aqui para referência; veja MDN Documentos da API WebSocket e documentação do Firebase para detalhes completos de implementação.
O padrão é sempre o mesmo: adquirir recursos, liberar recursos, declarar dependências honestamente. Se a configuração criar algo, a limpeza o destruirá. Se a configuração conectar, a limpeza será desconectada. Memorize esta simetria e cada novo efeito colateral se tornará uma aplicação direta do mesmo princípio.
Lista de verificação de revisão de relações públicas para limpeza de efeitos de uso
Esta lista de verificação foi projetada para ser inserida diretamente nas diretrizes de contribuição de uma equipe ou usada como um guia mental durante a revisão do código. Cada pergunta tem como alvo uma categoria específica de bug de limpeza.
- Será que cada
useEffectque cria um efeito colateral retorna uma função de limpeza? - A limpeza reverte totalmente a configuração (limpar o cronômetro, remover o ouvinte, abortar a busca)?
- As referências do manipulador de eventos são estáveis (função nomeada ou
useCallback) entãoremoveEventListenerrealmente corresponde? - É
AbortErrorcapturados e tratados separadamente de erros de rede genuínos (por exemplo, retorno antecipado sem definir estado de erro ou registro)? - A matriz de dependência inclui todos os valores referenciados dentro do efeito?
- O componente foi testado em montagem dupla React StrictMode?
- Existe algum
eslint-disable react-hooks/exhaustive-depscomentários? Em caso afirmativo, cada um deles é justificado com um comentário de código explicando por que a supressão é segura?
Um PR que passa em todas as sete verificações cobre as categorias de bugs relacionados à limpeza relatadas com mais frequência antes de chegarem à produção.
Suprimindo a regra Exhaustive-Deps Lint
O react-hooks/exhaustive-deps A regra ESLint existe para capturar dependências ausentes que causam fechamentos obsoletos. Desativar a regra oculta o bug em vez de corrigi-lo. O efeito fecha sobre um valor obsoleto e nunca é executado novamente em resposta à alteração desse valor, produzindo um fechamento obsoleto que é difícil de reproduzir e depurar. Quando a regra sinalizar uma dependência, adicione-a. Se isso causar repetições indesejadas, reestruture o efeito: extraia a lógica, divida em vários efeitos ou estabilize as referências com useCallback ou useMemo. A supressão deve ser o último recurso, sempre acompanhada de um comentário explicando por que a omissão é intencional e segura.
Retornando uma promessa em vez de uma função
Marcando o retorno de chamada do efeito como async é uma armadilha comum. Um async função retorna implicitamente uma promessa. React espera que o valor de retorno seja undefined ou uma função de limpeza síncrona. O React ignora uma promessa retornada e nunca registra uma função de limpeza, portanto, nada é executado na desmontagem ou alteração de dependência. A correção é sempre a mesma: defina uma função assíncrona interna dentro do efeito e invoque-a imediatamente, mantendo a função do efeito externo síncrona para que a instrução return forneça uma função de limpeza adequada.
O React ignora uma promessa retornada e nunca registra uma função de limpeza, portanto, nada é executado na desmontagem ou alteração de dependência.
Principais conclusões
- Todo
useEffectque adquire um recurso deve retornar uma função de limpeza que o libera. A única exceção: um efeito que não produz nenhum efeito colateral persistente (por exemplo, uma chamada única de análise do tipo disparar e esquecer). AbortControlleré a ferramenta moderna e nativa do navegador para cancelar solicitações de busca. Elimina condições de corrida sem sinalizadores booleanos ou contadores de sequência.- Adquira recursos, libere recursos, declare dependências honestamente. Esse princípio se aplica a temporizadores, ouvintes, buscas, WebSockets e assinaturas de terceiros.
- Coloque a lista de verificação de revisão de RP de sete itens em suas diretrizes de contribuição ou modelo de pull request. Ele detecta os bugs de limpeza mais comumente relatados antes que eles cheguem à produção.
Source link




