Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

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.

Rodolfo Mori14 min de leitura

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:

jsx
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>;
}
render — o parágrafo já está na tela? false efeito — o parágrafo já está na tela? true efeito — texto lido da tela: 3 pets na fila

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:

jsx
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>;
}
1) montagem sem array → rodou array vazio [] → rodou [naFila] → rodou com naFila = 3 2) naFila: 3 -> 4 sem array → rodou [naFila] → rodou com naFila = 4 3) busca: "" -> "rex" (naFila não muda) sem array → rodou 4) naFila: 4 -> 4 (mesmo valor)

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:

jsx
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>;
}
1) montagem render — naFila = 3 efeito — rodou 2) setNaFila(3) — mesmo valor, logo depois da montagem 3) setNaFila(4) — valor diferente render — naFila = 4 efeito — rodou 4) setNaFila(4) — mesmo valor, logo depois da troca render — naFila = 4 5) setNaFila(4) — mesmo valor outra vez

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.

js
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); })());
string igual : true número igual : true objeto "igual" : false array "igual" : false mesmo objeto : true

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:

jsx
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>;
}
-- monta com Rex efeito → assinou o prontuário de Rex -- troca para Mel limpeza → cancelou a assinatura de Rex efeito → assinou o prontuário de Mel -- troca para Fumaça limpeza → cancelou a assinatura de Mel efeito → assinou o prontuário de Fumaça -- desmonta o componente limpeza → cancelou a assinatura de Fumaça

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:

jsx
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>;
}
== dentro de <StrictMode>, sem função de limpeza efeito → abriu conexão. Abertas agora: 1 efeito → abriu conexão. Abertas agora: 2 resultado: conexões abertas = 2

Duas conexões abertas para um painel só. Agora o mesmo componente, com uma limpeza que fecha o que o efeito abriu.

jsx
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>;
}
== dentro de <StrictMode>, agora com limpeza efeito → abriu conexão. Abertas agora: 1 limpeza → fechou conexão. Abertas agora: 0 efeito → abriu conexão. Abertas agora: 1 resultado: conexões abertas = 1

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:

jsx
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>
  );
}
--- MONTAGEM 01 render PAI (primeiro = Rex) 02 render filho Rex 03 render filho Mel 04 efeito filho Rex 05 efeito filho Mel 06 efeito PAI (primeiro = Rex) --- ATUALIZAÇÃO: primeiro passa a ser Fumaça 07 render PAI (primeiro = Fumaça) 08 render filho Fumaça 09 render filho Mel 10 limpeza filho Rex 11 limpeza PAI (primeiro = Rex) 12 efeito filho Fumaça 13 efeito PAI (primeiro = Fumaça) --- DESMONTAGEM 14 limpeza PAI (primeiro = Fumaça) 15 limpeza filho Fumaça 16 limpeza filho Mel

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.

FilaDeAtendimento (pai) 1. render · 4. efeito CardDoPet (filho) 2. render · 3. efeito render desce efeito sobe

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.

jsx
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.

sem clearInterval | batidas até desmontar: 5 | batidas DEPOIS de desmontar: 5 com clearInterval | batidas até desmontar: 5 | batidas DEPOIS de desmontar: 0

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.

jsx
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.

== sem removeEventListener respondeu o efeito montado na sala 1 respondeu o efeito montado na sala 2 respondeu o efeito montado na sala 3 a conexão caiu UMA vez e a recepção recebeu 3 aviso(s) == com removeEventListener na limpeza respondeu o efeito montado na sala 3 a conexão caiu UMA vez e a recepção recebeu 1 aviso(s)

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.

jsx
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 flag ignorar | pet selecionado = Mel | tela mostra: prontuário de Rex com a flag ignorar | pet selecionado = Mel | tela mostra: prontuário de Mel

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:

jsx
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>;
}
setSegundos(segundos + 1) | depois de 300ms a tela mostra: Rex espera há 1s setSegundos((s) => s + 1) | depois de 300ms a tela mostra: Rex espera há 5s

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:

jsx
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>;
}
o que o usuário já viu na tela: espera acumulada: 0 min com efeito | renders: 2 | tela no fim: espera acumulada: 25 min sem efeito | renders: 1 | tela no fim: espera acumulada: 25 min

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:

jsx
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>;
}
Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render. [a mesma linha, mais 85 mil vezes] FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

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.

Ver todos os vídeos do canal
  • react
  • useeffect
  • hooks
  • ciclo de vida
  • strict mode

Perguntas frequentes

Qual a diferença entre useEffect e useLayoutEffect?
O useLayoutEffect roda depois que o React mexe no DOM, mas antes de o navegador pintar a tela — e ele bloqueia essa pintura. Use só quando precisar medir um elemento e ajustar o layout sem o usuário ver o passo intermediário. Para todo o resto, useEffect, que não segura a tela.
Dá para usar async direto na função do useEffect?
Não. O React espera que a função devolva ou nada, ou a função de limpeza, e uma função async sempre devolve uma Promise. Ele avisa na hora ("useEffect must not return anything besides a function") e, quando o componente sai da tela, tenta chamar a Promise como limpeza e quebra com "TypeError: destroy is not a function". Declare uma função async por dentro do efeito e chame ela na linha seguinte.
Preciso listar todas as dependências que o ESLint aponta?
Sim, e a saída certa quase nunca é silenciar o aviso. Se listar tudo provoca execução demais, o problema está antes: mova o valor para dentro do efeito, tire a criação do objeto de dentro do render, ou aceite que aquele código não era um efeito.
useEffect substitui componentDidMount e componentDidUpdate?
Ele cobre os mesmos momentos, mas a cabeça é outra. Em classe você pensava em fases do componente; com useEffect você declara com o que a tela precisa ficar sincronizada, e o array de dependências deriva as fases sozinho.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. React — useEffect — react.dev
  2. React — Synchronizing with Effects — react.dev
  3. React — You Might Not Need an Effect — react.dev

Continue por aqui