useEffect no React: efeito, dependências e limpeza
Quando o efeito roda, o que o array de dependências controla, por que existe a função de limpeza e como o Strict Mode roda tudo duas vezes.
Você troca de tela, mas o timer antigo continua contando. Clica no prontuário da Mel e, segundos depois, a resposta atrasada do Rex ocupa o lugar. Esses bugs surgem quando o componente abre algo fora do React e não sincroniza nem encerra esse trabalho no momento certo.
Aqui a gente começa observando um único efeito e avança até timer, evento do navegador e requisição. Os exemplos usam o painel de uma clínica veterinária e continuam o mesmo percurso da trilha de React.
O que é um efeito colateral no React?
É o trabalho que sincroniza um componente com um sistema externo, como timer, rede, evento do navegador ou biblioteca de terceiros. React é a biblioteca que executa componentes para descrever a interface; cada execução é um render. Calcular essa descrição faz parte do render. Conectar-se ao que está fora dela é um efeito colateral.
useEffect é um hook, uma função especial do React para declarar essa
sincronização. Pense numa sala de atendimento: o render define balcão, cadeira e
senha; o efeito liga relógio, rádio e painel externo; a limpeza desliga o que o
efeito ligou. O mapeamento ajuda a lembrar abertura e fechamento.
O limite da analogia é que a equipe física trabalha numa sequência visível,
enquanto o React agenda efeitos. Nem toda conta feita “depois” pertence ao hook:
filtrar dados e montar texto continuam no render. Antes de usar useEffect,
pergunte qual sistema externo será sincronizado. Se não houver nenhum, o efeito
provavelmente sobra.
Quando o useEffect roda em relação ao render, commit e pintura?
useEffect declara um efeito passivo, que roda depois do commit, etapa
em que o React aplica a interface ao DOM, a árvore de elementos mantida pelo
navegador. Em geral, um
efeito que não nasceu de uma interação espera a pintura da tela; o React pode
antecipá-lo em alguns casos de interação ou agendamento. Portanto, o contrato
seguro é “depois do commit”, não “sempre depois de o usuário ver os pixels”.
O exemplo pergunta se o elemento existe no DOM durante o render e dentro do efeito. Ele comprova o commit, não o instante da pintura:
import { useEffect, useState } from 'react';
function PainelRecepcao() {
const [naFila] = useState(3);
console.log('render — o parágrafo já está na tela?', document.getElementById('fila') !== null);
useEffect(() => {
console.log('efeito — o parágrafo já está na tela?', document.getElementById('fila') !== null);
console.log('efeito — texto lido da tela:', document.getElementById('fila').textContent);
});
return <p id="fila">{naFila} pets na fila</p>;
}Durante o render, o componente apenas descreve o parágrafo. Depois do commit, o
DOM já contém o elemento e o efeito consegue lê-lo. Daí a regra prática: ouvir
eventos e buscar dados são trabalhos de efeito; calcular o que aparece é
trabalho do render. Medição de layout antes da pintura exige outro mecanismo,
não uma suposição sobre o horário do useEffect.
O que muda entre efeito sem array, com [] e com dependências?
Sem array, o efeito acompanha cada commit. Com [], acompanha a montagem — a
entrada inicial do componente na página. Com dependências, também roda na
montagem e depois quando algum valor listado muda. O exemplo reúne as três formas
com a quantidade da fila e o texto da busca:
import { useEffect, useState } from 'react';
function PainelRecepcao() {
const [naFila, setNaFila] = useState(3);
const [busca, setBusca] = useState('');
useEffect(() => {
console.log(' sem array → rodou');
});
useEffect(() => {
console.log(' array vazio [] → rodou');
}, []);
useEffect(() => {
console.log(' [naFila] → rodou com naFila =', naFila);
}, [naFila]);
return <p>{naFila} pets na fila, busca: "{busca}"</p>;
}Na montagem, os três efeitos rodam. Quando só a busca muda, o de [naFila] fica
quieto porque sua dependência não mudou. No passo 4, pedir setNaFila(4) quando
o estado já vale 4 não produz novo commit nem efeito.
Isso não garante que a função do componente nunca seja chamada. O React pode renderizar e descartar o resultado sem fazer commit. O próximo teste registra render e efeito separadamente para mostrar essa diferença:
import { useEffect, useState } from 'react';
function ContadorDaFila() {
const [naFila, setNaFila] = useState(3);
console.log(' render — naFila =', naFila);
useEffect(() => {
console.log(' efeito — rodou');
});
return <p>{naFila} pets na fila</p>;
}Os passos 2 e 4 pedem o valor atual e não rodam efeito. No passo 4, porém, a função foi chamada e o resultado descartado. Essa diferença é uma otimização, não uma sequência para seu código controlar. O contrato útil é: sem commit, sem efeito. O artigo sobre re-render no React aprofunda o descarte.
| escrita | quando o efeito roda | serve para |
|---|---|---|
useEffect(fn) |
depois de cada commit | quase nada; costuma ser esquecimento |
useEffect(fn, []) |
depois do commit inicial | assinar algo que vale a vida inteira do componente |
useEffect(fn, [a, b]) |
no início e quando a ou b muda |
sincronizar com um dado que varia |
Experimente você mesmo
Cubra a saída do primeiro exemplo e preveja quais efeitos registram mensagem quando só a busca muda, quando a fila muda e quando recebe o mesmo valor. Depois confira e marque em quais casos houve commit.
Como o React compara as dependências?
Ele compara cada posição com Object.is. Esse algoritmo reconhece números e
strings de mesmo valor; objetos e arrays só são iguais quando apontam para a
mesma referência, mesmo que tenham conteúdo idêntico. Cada item listado é uma
dependência porque uma mudança nele pede nova sincronização.
console.log('string igual :', Object.is('Rex', 'Rex'));
console.log('número igual :', Object.is(12, 12));
console.log('objeto "igual" :', Object.is({ urgente: false }, { urgente: false }));
console.log('array "igual" :', Object.is([1, 2], [1, 2]));
console.log('mesmo objeto :', (() => { const f = { urgente: false }; return Object.is(f, f); })());Guarde essa terceira linha. Um objeto criado durante a renderização — um
const filtros = { urgentes: false } no corpo do componente — é um objeto
novo a cada render. Colocado nas dependências, ele nunca é igual ao anterior,
e o efeito roda sempre. É a origem da maioria dos loops, e o motivo de existirem
o useMemo e o useCallback.
Até aqui: render, commit e dependências
O render calcula; o commit aplica ao DOM; o efeito sincroniza depois do commit.
A lista de dependências decide quando repetir essa sincronização com base em
Object.is.
Quando a função de limpeza do useEffect roda?
Ela roda antes da próxima configuração quando uma dependência mudou e após o componente sair da tela. A limpeza é a função opcional devolvida pelo efeito; ela desfaz a sincronização criada por aquela execução. A sequência mostra qual pet cada limpeza enxerga:
import { useEffect, useState } from 'react';
function FichaDoPet() {
const [pet, setPet] = useState('Rex');
useEffect(() => {
console.log('efeito → assinou o prontuário de', pet);
return () => {
console.log('limpeza → cancelou a assinatura de', pet);
};
}, [pet]);
return <h2>{pet}</h2>;
}A limpeza da troca para Mel ainda diz “Rex”. Esse comportamento se chama
closure: a função conserva os valores do render em que foi criada. É assim
que clearInterval(id) alcança o timer certo e removeEventListener recebe a
mesma função assinada pelo efeito anterior.
Experimente você mesmo
Cubra a saída e acompanhe três ações: montar com Rex, trocar para Mel e desmontar. Antes de conferir, preveja a ordem de efeito e limpeza e o nome lembrado por cada closure.
Por que o Strict Mode executa o efeito duas vezes?
Porque, em desenvolvimento, o Strict Mode faz um ciclo extra de configuração e limpeza antes da configuração normal. Ele é uma ferramenta de diagnóstico, não um comportamento de produção. Se o efeito não consegue repetir esse par, também falhará quando a tela sair e voltar. Veja uma conexão sem limpeza:
import { useEffect } from 'react';
const conexoesAbertas = [];
function PainelDeSenhas() {
useEffect(() => {
conexoesAbertas.push('painel-senhas');
console.log('efeito → abriu conexão. Abertas agora:', conexoesAbertas.length);
}, []);
return <p>Chamando a próxima senha</p>;
}Duas conexões abertas para um painel só. Agora o mesmo componente, com uma limpeza que fecha o que o efeito abriu.
function PainelDeSenhas() {
useEffect(() => {
conexoesAbertas.push('painel-senhas');
console.log('efeito → abriu conexão. Abertas agora:', conexoesAbertas.length);
return () => {
conexoesAbertas.pop();
console.log('limpeza → fechou conexão. Abertas agora:', conexoesAbertas.length);
};
}, []);
return <p>Chamando a próxima senha</p>;
}O efeito continuou rodando duas vezes — e o resultado final está certo mesmo assim. É esse o ponto do Strict Mode.
Até aqui: efeito e limpeza formam um par
Cada configuração deve poder ser desfeita pela própria closure. Troca de dependência, desmontagem e o teste extra do Strict Mode exercitam esse mesmo par.
Qual é a ordem entre pai, filho, limpeza e efeito?
Nesta execução com React 19.2.8, os efeitos passivos dos filhos apareceram antes do efeito do pai na montagem; atualização e desmontagem tiveram sequências próprias. Isso é evidência do runtime testado, não contrato para coordenar componentes. A fila é o pai, os cards são filhos e o log registra a árvore:
import { useEffect, useState } from 'react';
let passo = 0;
const log = (texto) => console.log(String(++passo).padStart(2, '0'), texto);
function CardDoPet({ nome }) {
log(`render filho ${nome}`);
useEffect(() => {
log(`efeito filho ${nome}`);
return () => log(`limpeza filho ${nome}`);
}, [nome]);
return <li>{nome}</li>;
}
function FilaDeAtendimento() {
const [primeiro, setPrimeiro] = useState('Rex');
log(`render PAI (primeiro = ${primeiro})`);
useEffect(() => {
log(`efeito PAI (primeiro = ${primeiro})`);
return () => log(`limpeza PAI (primeiro = ${primeiro})`);
}, [primeiro]);
return (
<ul>
<CardDoPet nome={primeiro} />
<CardDoPet nome="Mel" />
</ul>
);
}Quatro leituras desta saída específica:
- Na montagem observada, o render desceu e o efeito subiu. O pai renderizou primeiro (01), os filhos depois (02, 03); os efeitos vieram na ordem inversa (04, 05, 06).
- Na atualização observada, as fases não se intercalaram. Vieram renders (07–09), limpezas (10–11) e novas configurações (12–13).
- A Mel renderizou e não teve efeito. A linha 09 mostra o card dela
renderizando de novo, porque o pai renderizou. Mas
[nome]continua valendo"Mel", então o efeito dela não rodou. Render e efeito são contadores separados. - Na desmontagem observada, o pai limpou antes dos filhos. As linhas 14–16 registram essa ordem nesta versão e neste ambiente.
Não use a ordem relativa entre pai e filho como canal de comunicação. O contrato seguro é local: o efeito só roda após commit, e sua limpeza precede a próxima configuração daquele efeito ou acompanha a saída do componente. Para coordenar componentes, prefira props, estado compartilhado ou uma API externa explícita.
Como limpar timer, listener e requisição no useEffect?
Use o mesmo par: o efeito inicia ou assina; a limpeza encerra, remove ou invalida o trabalho anterior. Timer, listener e requisição mudam de API, não de modelo.
Timer
O painel mostra há quanto tempo o Rex espera. O efeito liga um setInterval;
a limpeza chama clearInterval. Sem ela, o timer sobrevive ao componente.
import { useEffect, useState } from 'react';
let batidas = 0; // contador de fora do React, para a medição não sumir junto
function TempoDeEspera() {
const [segundos, setSegundos] = useState(0);
useEffect(() => {
const id = setInterval(() => {
batidas++;
setSegundos((s) => s + 1);
}, 50);
return () => clearInterval(id); // a linha que a primeira versão não tem
}, []);
return <span>esperando há {segundos}s</span>;
}Rodei o componente duas vezes, uma com a linha do return e outra sem. Em cada
execução o painel fica montado por 260ms, é desmontado, e o contador batidas
segue somando por mais 260ms.
O componente saiu da tela e o timer continuou batendo cinco vezes. Numa página em que a pessoa entra e sai da ficha vinte vezes, são vinte timers vivos disputando o mesmo processador.
Listener
A recepção precisa avisar quando a internet cai. O efeito assina o evento
offline do window; a limpeza remove aquela função exata. Aqui o componente
foi montado na sala 1 e trocou para a 2 e depois para a 3 — três execuções do
efeito — antes de a conexão cair uma única vez.
import { useEffect, useState } from 'react';
let avisos = 0;
function AvisoDeConexao() {
const [sala, setSala] = useState(1);
useEffect(() => {
const aoCairAConexao = () => {
avisos++;
console.log(` respondeu o efeito montado na sala ${sala}`);
};
window.addEventListener('offline', aoCairAConexao);
return () => window.removeEventListener('offline', aoCairAConexao);
}, [sala]);
return <p>sala {sala}</p>;
}De novo duas execuções, a segunda sem a linha do return.
Três avisos empilhados na tela para um evento só. E repare que os handlers
antigos ainda respondem citando a sala 1 e a sala 2 — eles congelaram o sala
do render em que nasceram, exatamente como a limpeza da seção anterior.
Requisição
Este é o mais traiçoeiro, porque o bug só aparece com a rede lenta. O prontuário do Rex é grande e demora 120ms; o da Mel vem em 20ms. A recepção clica no Rex e, antes de a resposta chegar, clica na Mel.
import { useEffect, useState } from 'react';
const espera = (ms) => new Promise((r) => setTimeout(r, ms));
const atraso = { Rex: 120, Mel: 20 }; // o prontuário do Rex é o mais pesado
async function buscarProntuario(pet) {
await espera(atraso[pet]);
return `prontuário de ${pet}`;
}
function Prontuario() {
const [pet, setPet] = useState('Rex');
const [texto, setTexto] = useState('carregando...');
useEffect(() => {
let ignorar = false;
setTexto('carregando...');
buscarProntuario(pet).then((resposta) => {
if (ignorar) return;
setTexto(resposta);
});
return () => { ignorar = true; };
}, [pet]);
return <p>{texto}</p>;
}Sem a limpeza, a resposta lenta chega por último e sobrescreve a certa: a tela
mostra o prontuário do Rex com a Mel selecionada. A limpeza não cancela a
requisição — ela marca a resposta antiga como irrelevante, que é o suficiente.
Para cancelar de verdade, o caminho é o AbortController, e ele aparece inteiro
na próxima lição sobre consumo de API.
Por que uma dependência ausente congela o valor?
Porque a closure continua lendo o valor do render antigo e [] impede o efeito
de ser configurado novamente. Silenciar o aviso do ESLint não atualiza essa
memória. Este intervalo lê segundos sem declará-lo:
import { useEffect, useState } from 'react';
function TempoDeEspera() {
const [segundos, setSegundos] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setSegundos(segundos + 1); // lê o segundos congelado no primeiro render
}, 50);
return () => clearInterval(id);
}, []);
return <span>Rex espera há {segundos}s</span>;
}O relógio travou em 1. A função dentro do intervalo nasceu no primeiro render, e
naquele render segundos valia 0 — ela vai calcular 0 + 1 para sempre.
Trocando por setSegundos((s) => s + 1), o próprio React entrega o valor mais
recente, e o efeito deixa de ler segundos. Quando o efeito realmente lê um
valor reativo, ele deve aparecer nas dependências; quando a função atualizadora
remove essa leitura, a dependência deixa de ser necessária.
Quando você não precisa de useEffect?
Quando o valor pode ser calculado durante o render sem sincronizar sistema externo. O painel precisa somar a espera da fila, mas a primeira versão transforma essa conta em efeito:
import { useEffect, useState } from 'react';
const pets = [
{ nome: 'Rex', espera: 12 },
{ nome: 'Mel', espera: 5 },
{ nome: 'Fumaça', espera: 8 },
];
let renders = 0; // só para contar quantas vezes cada versão renderizou
function ResumoComEfeito() {
const [total, setTotal] = useState(0);
renders++;
useEffect(() => {
console.log('o que o usuário já viu na tela:', document.getElementById('resumo').textContent);
setTotal(pets.reduce((soma, p) => soma + p.espera, 0));
}, []);
return <p id="resumo">espera acumulada: {total} min</p>;
}
function ResumoSemEfeito() {
renders++;
const total = pets.reduce((soma, p) => soma + p.espera, 0);
return <p id="resumo">espera acumulada: {total} min</p>;
}A primeira linha é o efeito lendo a tela antes de corrigir o número: o usuário chegou a ver “espera acumulada: 0 min”. Duas renderizações e um valor errado piscando, para calcular uma soma que cabia em uma linha do corpo do componente. A mesma regra vale para transformar o que veio de props e para derivar o estado de um formulário controlado.
Como nasce o erro Maximum update depth exceeded?
Ele nasce quando o efeito atualiza um estado que torna sua própria dependência
nova, repetindo efeito, setter e render sem parar. O exemplo combina um objeto
criado no render com setState dentro do efeito:
import { useEffect, useState } from 'react';
function ResumoDaFila() {
const [pets] = useState([{ nome: 'Rex', espera: 12 }, { nome: 'Mel', espera: 5 }]);
const [resumo, setResumo] = useState({ total: 0 });
useEffect(() => {
// objeto novo a cada execução: nunca é igual ao anterior
setResumo({ total: pets.reduce((soma, p) => soma + p.espera, 0) });
}, [pets, resumo]);
return <p>{pets.length} pets, {resumo.total} min de espera</p>;
}Não é força de expressão: a mensagem saiu 85.566 vezes antes de o Node abortar
sozinho, com 17 MB só de erro no terminal. No navegador, a aba trava em vez de
abortar. O ciclo é curto: o efeito cria um objeto novo, o objeto novo entra no
estado, o estado novo muda a dependência resumo, e o efeito roda outra vez.
As causas e as correções estão reunidas em
Maximum update depth exceeded.
Note que, neste exemplo, trocar { total: ... } por um número simples quebraria
o ciclo: guardando 17 em vez de { total: 17 }, o efeito roda duas vezes e
para — foi o resultado medido. Na segunda execução ele pede setTotal(17) com
17 já no estado, e o React abandona a atualização. O objeto novo garantia uma
referência diferente a cada volta; outros loops também podem existir com valores
primitivos quando o efeito continua produzindo um valor diferente.
O que estudar depois de useEffect?
Na ordem 12 da trilha vem consumir API no
React: você combina useState
com efeito, limpeza, AbortController e estados de carregamento e erro. É a
aplicação completa da requisição que começou nesta lição.
Se quiser revisar o caminho antes de avançar, o guia de React mostra onde cada hook entra.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
Qual a diferença entre useEffect e useLayoutEffect?
Dá para usar async direto na função do useEffect?
Preciso listar todas as dependências que o ESLint aponta?
useEffect substitui componentDidMount e componentDidUpdate?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, React 19.2.8, jsdom 30.0.1, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- React — useEffect — react.dev
- React — Synchronizing with Effects — react.dev
- React — You Might Not Need an Effect — react.dev



