Frontend

6 Técnicas Secretas do useEffect

Além do fetch e do event listener. O que a documentação mostra — e o que ela não explica direito.

6 Técnicas Secretas do useEffect

O useEffect é o hook mais ensinado. E o mais mal entendido. Não porque a API seja ruim — mas porque a maioria dos tutoriais para no segundo exemplo.

Fetch na montagem. Listener de evento. Array de dependências com a variável certa. Isso é o básico, e o básico está em todo lugar.

O que não está em todo lugar são os seis comportamentos que separam quem usa o useEffect de quem entende o useEffect.

Bugs evitados
6
patterns de erro recorrentes
React 19
1
técnica do futuro já disponível
Serviço
0
Fluxo crítico de conversão.

Técnica 01 — Cleanup com AbortController

Toda vez que um componente desmonta antes do fetch terminar, a resposta ainda chega — e tenta atualizar o estado de um componente que não existe mais.

O React lançava o aviso no console: "Can't perform a React state update on an unmounted component". O aviso foi removido no React 18. O bug continua.

MONTA useEffect dispara fetch() DESMONTA cleanup rodou? resposta chega setState() SEM abort → bug silencioso COM AbortController → cancelado ✓
Diagrama — Ciclo de vida com fetch assíncrono
JS
function UserProfile({ userId }) {  const [user, setUser] = useState(null);  useEffect(() => {    const controller = new AbortController();    async function fetchUser() {      try {        const res = await fetch(`/api/users/${userId}`, {          signal: controller.signal,        });        const data = await res.json();        setUser(data);      } catch (err) {        if (err.name === 'AbortError') return;        console.error(err);      }    }    fetchUser();    return () => controller.abort();  }, [userId]);  return <div>{user?.name}</div>;}

Técnica 02 — O array de dependências é uma declaração, não um filtro

O instinto errado aparece assim: o efeito roda demais, então o dev remove uma dependência do array para "parar" as execuções extras. Funciona até o dia que não funciona.

O array de dependências diz ao React quais valores esse efeito usa. Não quais valores devem ativar o efeito. Essa distinção muda tudo. Omitir uma dependência não silencia o re-run — cria um stale closure: o efeito fica preso num snapshot antigo do valor, e o bug aparece de forma não determinística.

❌ Stale closure

useEffect(() => {
// count é lido aqui,
// mas foi omitido do array.
// O valor nunca atualiza.
document.title = Count: ${count};
}, []); // ← mentira pro React

✅ Declaração honesta

useEffect(() => {
// count está no array.
// React sabe quando re-executar.
document.title = Count: ${count};
}, [count]); // ← contrato honesto

Se o efeito roda demais, o problema real geralmente é um objeto ou função sendo recriado a cada render. A solução é estabilizar os valores — com useCallback, useMemo, ou movendo a definição pra dentro do próprio efeito. Não omitir.

Técnica 03 — useLayoutEffect: quando o DOM não pode esperar

O useEffect dispara depois da pintura do navegador. Na maioria dos casos, é exatamente o que você quer. Mas existe uma janela entre o React comitar as mudanças no DOM e o navegador pintar a tela.

Em posicionamento de tooltips, medições de layout e animações que dependem de tamanho real — o useEffect dispara tarde demais. O usuário vê o elemento na posição errada antes de corrigir.

TEMPO → useEffect React render DOM commit Pintura useEffect dispara useLayoutEffect React render DOM commit useLayoutEffect Pintura flash visível aqui ↑ síncrono, antes da pintura ✓ ⚠ useLayoutEffect bloqueia a pintura. Não faça operações lentas aqui.
Diagrama — Timing: useEffect vs useLayoutEffect
JS
function Tooltip({ targetRef, children }) {  const tooltipRef = useRef(null);  const [pos, setPos] = useState({ top: 0, left: 0 });  // Mede ANTES da pintura — sem flash de posição incorreta  useLayoutEffect(() => {    if (!targetRef.current || !tooltipRef.current) return;    const targetRect = targetRef.current.getBoundingClientRect();    const tooltipRect = tooltipRef.current.getBoundingClientRect();    setPos({      top: targetRect.top - tooltipRect.height - 8,      left: targetRect.left + targetRect.width / 2 - tooltipRect.width / 2,    });  }, [targetRef]);  return (    <div ref={tooltipRef} style={{ position: 'fixed', top: pos.top, left: pos.left }}>      {children}    </div>  );}

Técnica 04 — Refs como válvula de escape das dependências

Funções de callback chegam como props. Quando você precisa chamá-las dentro de um efeito, o linter exige que entrem no array de dependências. Mas callbacks recriados a cada render fazem o efeito reexecutar sem motivo real.

A ref é a saída: guarda o valor mais recente sem fazer o efeito depender dele.

A) Dependência direta — re-runs desnecessários Render 1 Render 2 Render 3 re-run re-run ... e por aí vai B) Via ref — efeito estável Render 1 Render 2 Render 3 callbackRef sempre atualizado efeito: roda 1x ✓
Diagrama — Callback via ref vs. dependência direta
JS
function useInterval(callback, delay) {  // Ref sempre com a versão mais recente do callback  const savedCallback = useRef(callback);  // Atualiza a ref sem re-executar o efeito  useEffect(() => {    savedCallback.current = callback;  }, [callback]);  // O interval usa a ref — array de deps só precisa de [delay]  useEffect(() => {    if (delay === null) return;    const id = setInterval(() => {      savedCallback.current(); // lê o valor mais recente via ref    }, delay);    return () => clearInterval(id);  }, [delay]);}

Técnica 05 — useEffectEvent: ler estado sem depender dele

Um efeito precisa reagir a uma mudança — roomId — mas dentro dele, precisa ler um valor atual — theme — sem que esse valor cause reexecução. O React 19 introduziu useEffectEvent para formalizar essa separação entre código reativo e não-reativo.

JS
import { experimental_useEffectEvent as useEffectEvent } from 'react';function ChatRoom({ roomId, theme }) {  // onConnected lê theme por dentro, mas não depende dele  const onConnected = useEffectEvent(() => {    showNotification(`Conectado à sala ${roomId}`, theme);  });  useEffect(() => {    const connection = createConnection(roomId);    connection.on('connected', () => onConnected());    connection.connect();    return () => connection.disconnect();  }, [roomId]); // theme não precisa estar aqui}
useEffect reagir a: [roomId] conectar / desconectar reativo por natureza chama useEffectEvent lê: theme (sempre atual) showNotification() não-reativo — não causa re-run ✓
Diagrama — Separação reativo / não-reativo

Técnica 06 — useEffect não gerencia estado derivado

Esse é o erro silencioso mais comum. O código funciona. Os testes passam. E ainda assim tem um render a mais toda vez — além de um useEffect que não deveria existir.

JS
// ❌ Effect para derivar estado — causa render duplofunction FilteredList({ items, query }) {  const [filtered, setFiltered] = useState([]);  useEffect(() => {    setFiltered(items.filter(item => item.name.includes(query)));  }, [items, query]);  return <List items={filtered} />;}// ✅ Calculado no render — correto e sem overheadfunction FilteredList({ items, query }) {  const filtered = useMemo(    () => items.filter(item => item.name.includes(query)),    [items, query]  );  return <List items={filtered} />;}

useEffect existe para sincronizar o React com sistemas externos: APIs, WebSockets, timers, localStorage, bibliotecas de terceiros. Não para transformar dados que já estão dentro do React.

useState + useEffect Render 1 lista desatualizada Render 2 lista correta (agora) = 2 renders por query change useMemo no render Render único — correto ✓ = 1 render por query change
Diagrama — Quantidade de renders

O useEffect não mudou muito desde o React 16.8. O que mudou é o entendimento do que ele realmente é: uma camada de sincronização entre o React e o mundo externo.

Tudo que não precisa do mundo externo — não pertence a ele.

Quiz interativo

Teste seu conhecimento

Responda e valide seu entendimento.

1. Qual API o artigo recomenda para cancelar requisições no cleanup do useEffect?
2. Segundo o artigo, o array de dependências é...
3. Quando usar useLayoutEffect no lugar de useEffect?