Efeitos com useEffect — fora do render.
Quando e por que usar useEffect. Array de dependências, função de cleanup e como evitar efeitos desnecessários.
Efeitos são operações que saem do ciclo de renderização puro: buscar dados de uma API, sincronizar com localStorage, adicionar listeners de evento, inicializar bibliotecas DOM. useEffect é o hook que permite executar essas operações depois que o React atualizou o DOM — de forma sincronizada com o ciclo de vida do componente.
Quando useEffect é necessário
A regra prática: se uma operação não é uma transformação de dados (que você pode calcular durante o render) e não é um evento de usuário (que vai para um handler), provavelmente é um efeito.
O caso mais comum é buscar dados ao montar o componente:
import { useState, useEffect } from "react";
import type { Artigo } from "../types";
export function ArticleList() {
const [artigos, setArtigos] = useState<Artigo[]>([]);
const [carregando, setCarregando] = useState(true);
const [erro, setErro] = useState<string | null>(null);
useEffect(() => {
// roda depois que o componente aparece na tela
fetch("/artigos.json")
.then(res => {
if (!res.ok) throw new Error(`Erro ${res.status}`);
return res.json();
})
.then((dados: Artigo[]) => {
setArtigos(dados);
setCarregando(false);
})
.catch(err => {
setErro(err.message);
setCarregando(false);
});
}, []); // [] — roda apenas na montagem
if (carregando) return <p>Carregando artigos...</p>;
if (erro) return <p>Erro: {erro}</p>;
return (
<section>
{artigos.map(artigo => (
<ArticleCard key={artigo.id} artigo={artigo} />
))}
</section>
);
} O que você não deve fazer em useEffect: derivar estado de props. Se você tem uma prop artigos e quer filtrar, faça isso durante o render — não em um efeito que copia para outro estado:
// ❌ useEffect desnecessário para derivar estado
function ArticleList({ artigos, query }: ArticleListProps) {
const [artigosFiltrados, setArtigosFiltrados] = useState(artigos);
useEffect(() => {
setArtigosFiltrados(artigos.filter(a => a.titulo.includes(query)));
}, [artigos, query]); // render extra — componente renderiza duas vezes
return <>{artigosFiltrados.map(a => <ArticleCard key={a.id} artigo={a} />)}</>;
}
// ✅ calcular durante o render — sem estado extra, sem efeito
function ArticleList({ artigos, query }: ArticleListProps) {
const artigosFiltrados = artigos.filter(a =>
a.titulo.toLowerCase().includes(query.toLowerCase())
);
return <>{artigosFiltrados.map(a => <ArticleCard key={a.id} artigo={a} />)}</>;
} Sintaxe e array de dependências
useEffect aceita dois argumentos: a função de efeito e o array de dependências.
// sem array — roda após todo render (quase nunca é o que você quer)
useEffect(() => {
console.log("renderizou");
});
// array vazio — roda apenas na montagem
useEffect(() => {
console.log("montou");
}, []);
// array com dependências — roda quando qualquer dependência muda
useEffect(() => {
console.log("query mudou para", query);
}, [query]); As dependências devem incluir tudo que o efeito usa e que pode mudar entre renders: props, estado, funções definidas no componente. O ESLint com o plugin eslint-plugin-react-hooks avisa quando dependências estão faltando:
export function SearchResults({ query }: { query: string }) {
const [resultados, setResultados] = useState<Artigo[]>([]);
useEffect(() => {
if (!query) {
setResultados([]);
return;
}
fetch(`/api/busca?q=${encodeURIComponent(query)}`)
.then(res => res.json())
.then(setResultados);
}, [query]); // query está no array — efeito roda quando query muda
return (
<ul>
{resultados.map(r => <li key={r.id}>{r.titulo}</li>)}
</ul>
);
} Função de cleanup
O retorno de useEffect é a função de cleanup — React a executa antes de rodar o efeito novamente (quando as dependências mudam) e quando o componente desmonta. É essencial para cancelar operações pendentes e evitar memory leaks.
O problema sem cleanup: se SearchResults desmonta enquanto um fetch ainda está pendente, o fetch completa, chama setResultados, e React tenta atualizar um componente que não existe mais — aviso no console e potencial bug.
Com AbortController, o fetch é cancelado quando o efeito limpa:
export function SearchResults({ query }: { query: string }) {
const [resultados, setResultados] = useState<Artigo[]>([]);
const [carregando, setCarregando] = useState(false);
useEffect(() => {
if (!query) {
setResultados([]);
return;
}
const controller = new AbortController();
setCarregando(true);
fetch(`/api/busca?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
})
.then(res => res.json())
.then(dados => {
setResultados(dados);
setCarregando(false);
})
.catch(err => {
// AbortError é esperado quando o cleanup cancela o fetch
if (err.name !== "AbortError") {
console.error("Erro na busca:", err);
setCarregando(false);
}
});
// cleanup — cancela o fetch anterior quando query muda ou componente desmonta
return () => {
controller.abort();
};
}, [query]);
if (carregando) return <p>Buscando...</p>;
return <ul>{resultados.map(r => <li key={r.id}>{r.titulo}</li>)}</ul>;
} Isso resolve o problema de race condition: se o usuário digita “css” e imediatamente depois “html”, dois fetches rodam em paralelo. O fetch de “css” pode completar depois do de “html” — sem cleanup, os resultados de “css” sobrescreveriam os de “html”. Com AbortController, o fetch de “css” é cancelado quando a query muda para “html”.
Outros usos comuns de cleanup:
// limpar um intervalo ao desmontar
useEffect(() => {
const id = setInterval(() => {
setTempo(prev => prev + 1);
}, 1000);
return () => clearInterval(id);
}, []);
// desinscrever de evento do DOM ao desmontar
useEffect(() => {
function handleResize() {
setLargura(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []); Sincronizar com localStorage
useEffect é a forma correta de sincronizar estado com o localStorage — que é externo ao React:
export function useTema() {
const [tema, setTema] = useState<"claro" | "escuro">(() => {
// função como valor inicial — roda apenas uma vez, na montagem
const salvo = localStorage.getItem("tema");
return salvo === "escuro" ? "escuro" : "claro";
});
useEffect(() => {
// sincronizar com localStorage e com o atributo do documento quando tema muda
localStorage.setItem("tema", tema);
document.documentElement.setAttribute("data-theme", tema);
}, [tema]);
return { tema, setTema };
} O valor inicial usa a forma funcional do useState — a função passada como argumento roda apenas uma vez, na montagem, lendo o localStorage sem causar renders extras. O useEffect então mantém o localStorage e o atributo do documento sincronizados quando tema muda.
Resumo
- Efeitos são operações fora do render puro: fetch,
localStorage, listeners, timers. - Sem array: roda após todo render.
[]: roda apenas na montagem.[dep]: roda quando a dependência muda. - Não use
useEffectpara derivar estado de props — calcule durante o render, sem estado extra. - Função de cleanup: retornada pelo efeito, executa antes do próximo efeito ou na desmontagem. Use para cancelar fetches com
AbortController, limpar timers e remover listeners. - Race conditions:
AbortControllergarante que apenas o último fetch importa quando a query muda rapidamente. - Dependências honestas: declare tudo que o efeito usa e pode mudar. O ESLint com
react-hooksavisa quando faltam.
Qual é a diferença entre useEffect sem array de dependências e useEffect com array vazio []?
Para que serve a função de cleanup retornada pelo useEffect?
Por que você não deve usar useEffect para derivar estado de props?
O que acontece se você não declarar todas as dependências que o efeito usa no array de dependências?
Aula concluída
Quase lá.