Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// O padrão principal: todo useEffect que aloca deve limpar
useEffect(() => {
// ALOCAÇÃO: subscription, listener, timer, fetch
const controller = new AbortController();
const intervalId = setInterval(pollData, 5000);
window.addEventListener("resize", handleResize);
fetchData({ signal: controller.signal });
// LIMPEZA: executa ao desmontar e antes de reexecutar o efeito
return () => {
controller.abort();
clearInterval(intervalId);
window.removeEventListener("resize", handleResize);
};
}, []);Quando usar isso: Quando o uso de memória do seu aplicativo cresce ao longo do tempo durante a navegação, quando o Chrome DevTools mostra um tamanho crescente de heap JS, ou quando os usuários relatam que o aplicativo fica mais lento quanto mais tempo o usam.
// ---- ANTES: Componente com 5 vazamentos de memória ----
function LiveDashboard({ userId }: { userId: string }) {
const [data, setData] = useState<DashboardData | null>(null);
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
const [notifications, setNotifications] = useState<string[]>([]);
const chartRef = useRef<HTMLCanvasElement>(null);
// VAZAMENTO 1: Fetch sem abortar - atualiza o estado após desmontar
useEffect(() => {
async function loadData() {
const res = await fetch(`/api/dashboard/${userId}`);
const json = await res.json();
setData(json); // Pode executar após desmontar!
}
loadData();
}, [userId]);
// VAZAMENTO 2: Intervalo nunca limpo - continua rodando após desmontar
useEffect(() => {
setInterval(async () => {
const res = await fetch(`/api/dashboard/${userId}/live`);
const json = await res.json();
setData(json); // Roda para sempre, mesmo após navegar para longe
}, 5000);
}, [userId]);
// VAZAMENTO 3: Event listener nunca removido
useEffect(() => {
window.addEventListener("mousemove", (e) => {
setMousePos({ x: e.clientX, y: e.clientY });
});
}, []);
// VAZAMENTO 4: WebSocket nunca fechado
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/ws/${userId}`);
ws.onmessage = (event) => {
setNotifications((prev) => [...prev, event.data]);
};
// Sem limpeza - WebSocket permanece aberto
}, [userId]);
// VAZAMENTO 5: Contexto do canvas e dados grandes retidos via closure
useEffect(() => {
const ctx = chartRef.current?.getContext("2d");
const hugeDataSet = new Float64Array(1_000_000); // Alocação de 8MB
function drawChart() {
// Closure retém hugeDataSet mesmo após o componente desmontar
ctx?.clearRect(0, 0, 800, 400);
// ... desenha usando hugeDataSet
}
drawChart();
// hugeDataSet nunca é liberado porque drawChart retém uma referência
}, [data]);
return (
<div>
<canvas ref={chartRef} width={800} height={400} />
<p>Mouse: {mousePos.x}, {mousePos.y}</p>
<ul>
{notifications.map((n, i) => (
<li key={i}>{n}</li>
))}
</ul>
</div>
);
}
// ---- DEPOIS: Todos os 5 vazamentos corrigidos - limpeza adequada em cada efeito ----
function LiveDashboard({ userId }: { userId: string }) {
const [data, setData] = useState<DashboardData | null>(null);
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
const [notifications, setNotifications] = useState<string[]>([]);
const chartRef = useRef<HTMLCanvasElement>(null);
// CORREÇÃO 1: AbortController cancela fetch ao desmontar ou mudar userId
useEffect(() => {
const controller = new AbortController();
async function loadData() {
try {
const res = await fetch(`/api/dashboard/${userId}`, {
signal: controller.signal,
});
const json = await res.json();
setData(json);
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") {
// Esperado ao desmontar o componente - não é um erro
return;
}
throw err;
}
}
loadData();
return () => controller.abort();
}, [userId]);
// CORREÇÃO 2: Intervalo limpo ao desmontar, AbortController para cada poll
useEffect(() => {
const controller = new AbortController();
const intervalId = setInterval(async () => {
try {
const res = await fetch(`/api/dashboard/${userId}/live`, {
signal: controller.signal,
});
const json = await res.json();
setData(json);
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") return;
console.error("Poll falhou:", err);
}
}, 5000);
return () => {
clearInterval(intervalId);
controller.abort();
};
}, [userId]);
// CORREÇÃO 3: Handler nomeado + removeEventListener na limpeza
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setMousePos({ x: e.clientX, y: e.clientY });
};
window.addEventListener("mousemove", handleMouseMove);
return () => window.removeEventListener("mousemove", handleMouseMove);
}, []);
// CORREÇÃO 4: WebSocket fechado na limpeza
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/ws/${userId}`);
ws.onmessage = (event) => {
setNotifications((prev) => [...prev, event.data]);
};
ws.onerror = (error) => {
console.error("Erro no WebSocket:", error);
};
return () => {
ws.close();
};
}, [userId]);
// CORREÇÃO 5: Limpeza anula dados grandes, usa ref para binding mutável
useEffect(() => {
const ctx = chartRef.current?.getContext("2d");
let hugeDataSet: Float64Array | null = new Float64Array(1_000_000);
function drawChart() {
if (!hugeDataSet || !ctx) return;
ctx.clearRect(0, 0, 800, 400);
// ... desenha usando hugeDataSet
}
drawChart();
return () => {
hugeDataSet = null; // Libera 8MB - permite a coleta de lixo
};
}, [data]);
return (
<div>
<canvas ref={chartRef} width={800} height={400} />
<p>Mouse: {mousePos.x}, {mousePos.y}</p>
<ul>
{notifications.map((n, i) => (
<li key={i}>{n}</li>
))}
</ul>
</div>
);
}O que isso demonstra:
clearInterval para o polling quando o componente não está mais visívelremoveEventListeneruseEffect executa em dois cenários: (1) antes que o efeito seja reexecutado devido a mudanças nas dependências, e (2) quando o componente é desmontado. É aqui que toda a desalocação de recursos deve acontecer.setState em um componente desmontado é um no-op no React 18+, mas a própria closure impede a coleta de lixo de tudo o que ela referencia.useEffect sem limpeza significa que cada remontagem (navegação) adiciona outro listener. Após 10 navegações, você tem 10 listeners disparando em cada evento.AbortController para múltiplos fetches concorrentes:
useEffect(() => {
const controller = new AbortController();
async function loadAll() {
const [users, orders, metrics] = await Promise.all([
fetch("/api/users", { signal: controller.signal }),
fetch("/api/orders", { signal: controller.signal }),
fetch("/api/metrics", { signal: controller.signal }),
]);
// Um abort cancela todos os três
setData({
users: await users.json(),
orders: await orders.json(),
metrics: await metrics.json(),
});
}
loadAll().catch((err) => {
if (err.name !== "AbortError") throw err;
});
return () => controller.abort();
}, []);WeakRef para referências opcionais:
// Avançado: WeakRef permite a coleta de lixo do objeto referenciado
function useWeakCallback<T extends object>(target: T, callback: (target: T) => void) {
const weakRef = useRef(new WeakRef(target));
useEffect(() => {
const intervalId = setInterval(() => {
const obj = weakRef.current.deref();
if (obj) {
callback(obj);
} else {
// Objeto foi coletado pelo garbage collector - pare o polling
clearInterval(intervalId);
}
}, 1000);
return () => clearInterval(intervalId);
}, [callback]);
}Detectando vazamentos com snapshots de heap:
1. Abra o Chrome DevTools -> Aba Memory
2. Tire um Heap Snapshot (Snapshot 1 - linha de base)
3. Navegue para a página suspeita de vazamento
4. Navegue para fora da página
5. Clique no botão de coleta de lixo (ícone de lixeira)
6. Tire um Heap Snapshot (Snapshot 2)
7. Selecione o Snapshot 2 -> Mude a visualização para "Comparison"
8. Ordene pela coluna "Delta" - objetos com deltas positivos são vazamentos potenciais
9. Procure por: Detached HTMLDivElement, EventListener, Closure, Array
AbortController e AbortSignal são nativos da biblioteca DOM. Nenhum tipo adicional é necessário.void ou undefined (useEffect proíbe retornar qualquer outra coisa).WeakRef<T> requer T extends object. Primitivos não podem ser referenciados fracamente.FinalizationRegistry<T> recebe o valor mantido do tipo T quando o alvo é coletado.Funções anônimas impedem a limpeza - window.addEventListener("resize", () => {...}) não pode ser removido porque removeEventListener requer a mesma referência de função. Correção: Sempre use funções nomeadas ou armazene a referência em uma variável.
Montagem dupla do Strict Mode mascarando vazamentos - O React 18+ Strict Mode monta, desmonta e remonta cada componente em desenvolvimento. Isso pode fazer com que vazamentos pareçam comportamento "normal". Correção: Um componente devidamente limpo funciona corretamente durante o ciclo de montagem do Strict Mode. Se você vir listeners ou conexões duplicados, sua limpeza está incompleta.
Dependência ausente no efeito causa limpeza obsoleta - Se a função de limpeza referenciar uma variável que não está no array de dependências, ela pode fechar sobre um valor obsoleto. Correção: Inclua todas as variáveis referenciadas no array de dependências, ou use refs para valores mutáveis que não devem acionar reexecuções.
Limpeza de bibliotecas de terceiros - Bibliotecas como chart.js, mapbox-gl ou players de vídeo alocam recursos internos. Correção: Chame o método destroy/dispose da biblioteca na função de limpeza: chart.destroy(), map.remove(), player.dispose().
React 18+ não avisa mais sobre atualizações de estado em componentes desmontados - O aviso foi removido porque produzia muitos falsos positivos. Mas o vazamento subjacente ainda existe. Correção: Use AbortController e funções de limpeza independentemente de o React avisar você.
Timers com closure sobre arrays crescentes - Callbacks de setInterval que adicionam elementos a um array criam uma estrutura de dados em constante crescimento, mesmo que o componente seja remontado com um estado novo. Correção: Limpe o intervalo na limpeza e garanta que a closure não referencie acumuladores de montagens anteriores.
| Abordagem | Compromisso |
|---|---|
Limpeza do useEffect | Integrado; requer disciplina manual para cada efeito |
| AbortController | API padrão; funciona apenas com fetch e APIs que aceitam AbortSignal |
| TanStack Query | Gerenciamento automático do ciclo de vida do fetch; adiciona dependência |
| SWR | Deduplicação e limpeza automática de requisições; adiciona dependência |
Hook use (React 19) | Baseado em Promise; funciona com Suspense, sem necessidade de limpeza manual |
| RxJS | Assinaturas de Observable com limpeza automática; dependência pesada |
| Assinaturas do Zustand | Gerenciado pelo store; limpeza através do valor de retorno de subscribe |
Todo useEffect que aloca recursos deve retornar uma função de limpeza:
useEffect(() => {
const controller = new AbortController();
fetchData({ signal: controller.signal });
return () => controller.abort();
}, []);A limpeza é executada ao desmontar e antes que o efeito seja reexecutado.
O aviso foi removido porque produzia muitos falsos positivos. No entanto, o vazamento subjacente ainda existe -- a closure retém referências ao estado do componente e impede a coleta de lixo. Use AbortController e funções de limpeza independentemente.
Use AbortController e passe seu sinal para fetch:
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then((r) => r.json())
.then(setData)
.catch((err) => {
if (err.name === "AbortError") return;
throw err;
});
return () => controller.abort();
}, [url]);window.addEventListener("resize", () => {...}) não pode ser removido porque removeEventListener requer a mesma referência de função exata.
Correção: Armazene o handler em uma variável:
const handleResize = () => { /* ... */ };
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);O React 18+ Strict Mode monta, desmonta e remonta cada componente em desenvolvimento. Isso pode fazer com que vazamentos pareçam comportamento "normal". Um componente devidamente limpo funciona corretamente durante este ciclo -- se você vir listeners ou conexões duplicados, sua limpeza está incompleta.
Anule grandes dados na função de limpeza para permitir a coleta de lixo:
useEffect(() => {
let hugeData: Float64Array | null = new Float64Array(1_000_000);
drawChart(hugeData);
return () => { hugeData = null; };
}, [data]);Feche o WebSocket na função de limpeza:
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/ws/${id}`);
ws.onmessage = (event) => setData(event.data);
return () => ws.close();
}, [id]);WeakRef<T> requer T extends object. Primitivos (strings, números, booleanos) não podem ser referenciados fracamente. O tipo de retorno da função de limpeza de useEffect deve ser void ou undefined.
Chame o método destroy/dispose da biblioteca na função de limpeza:
useEffect(() => {
const chart = new Chart(canvasRef.current, config);
return () => chart.destroy();
}, [config]);Bibliotecas que manipulam o DOM diretamente retêm nós desanexados se não forem destruídas corretamente.
Sim. Passe o mesmo signal para todas as chamadas fetch. Chamar controller.abort() uma vez cancela todas as requisições que usam esse sinal:
const [users, orders] = await Promise.all([
fetch("/api/users", { signal: controller.signal }),
fetch("/api/orders", { signal: controller.signal }),
]);O callback continua adicionando elementos ao array, mesmo que o componente seja remontado com um estado fresco. A closure antiga retém o array antigo, criando uma estrutura de dados em constante crescimento. Sempre limpe o intervalo na limpeza.
Revisado por Chris St. John·Última atualização: 10 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥