Você provavelmente não precisa de useEffect
Seis casos em que useEffect é a ferramenta errada e o que usar no lugar: render, key, event handler, useMemo, lib de dados e useSyncExternalStore.
Tudo começa com um componente simples
Imagina uma tela de perfil. Ela recebe um `userId`, mostra o nome da pessoa e tem um campo de comentário. Nada demais.
Aí vem o pedido: "quando trocar de usuário, limpa o comentário". Você pensa: quando o `userId` muda, eu faço alguma coisa. E o React tem um hook que parece ter sido feito exatamente pra isso:
useEffect(() => {
setComment('');
}, [userId]);
Funciona. Você segue o baile.
Uma semana depois pedem para filtrar a lista quando o filtro muda. Você escreve outro useEffect. Depois um para recalcular o total. Depois um para avisar o componente pai. Quando você olha, metade do componente é useEffect, a tela pisca com o valor velho por um instante e você abre o DevTools tentando entender por que isso renderizou mais vezes do que deveria.
Eu já caí nessa. Num painel que eu estava montando, tinha uma lista com filtro e um total no rodapé. Toda vez que o filtro mudava, um useEffect recalculava o total e guardava num estado. Na tela, o total aparecia com o número antigo por um piscar de olho e só depois atualizava. Passei um tempo achando que era bug de cache, até abrir o React DevTools e ver o componente renderizando duas vezes a cada clique. Era eu, chamando setState dentro de um effect pra guardar uma conta que cabia numa linha.
Pra entender por que isso acontece, antes de falar do que evitar, vale lembrar o que o useEffect faz de verdade.
O que o useEffect faz?
Todo componente React é uma função. O React chama essa função para descobrir o que mostrar na tela. Isso é o render.
O render deveria ser só conta: mesmas props e mesmo estado, mesmo resultado. Sem buscar nada, sem abrir conexão, sem mexer em coisa de fora.
Depois que o React atualiza a tela, ele roda os effects. É a parte do ciclo que o useEffect ocupa: "depois que a tela atualizou, faça isso".
useEffect(() => {
const socket = new WebSocket(`wss://exemplo.com/salas/${roomId}`);
return () => socket.close();
}, [roomId]);
Três peças:
- A função: o que rodar depois que a tela atualizou. Aqui, abrir a conexão. - O array de dependências: quando rodar de novo. Aqui, sempre que o `roomId` mudar. - O retorno (cleanup): como desfazer antes de rodar de novo ou quando o componente sair. Aqui, fechar a conexão.
Repara no que esse exemplo faz: ele fala com algo fora do React, uma conexão de rede. O React não sabe abrir nem fechar isso sozinho, então você precisa de um lugar para dizer "quando esse componente aparecer, abra; quando sair, feche". O useEffect é esse lugar. A documentação do React chama de saída de emergência, uma forma de sair do React para sincronizar com um sistema externo.
Onde a conta não fecha
Agora volta no exemplo do perfil. Limpar o comentário quando o `userId` muda fala com alguma coisa fora do React? Não. É estado reagindo a estado, tudo dentro do próprio React. O mesmo vale para o filtro da lista, para o total e para avisar o pai.
É aí que o effect vira ferramenta errada: você usa uma saída de emergência para resolver algo que nunca saiu de casa. O custo é render a mais, valor velho aparecendo na tela por um instante e código mais difícil de seguir.
Neste artigo: seis casos em que o effect é a ferramenta errada, o que usar no lugar e quando ele continua sendo a escolha certa.
A regra de bolso
Antes de escrever um `useEffect`, pergunte: isso está sincronizando com algo fora do React? (rede, DOM direto, timer, WebSocket, uma biblioteca de terceiros). Se a resposta for não, o código provavelmente pertence ao render ou a um event handler.
1. Estado derivado: calcule no render
O clássico. Você tem `firstName` e `lastName` e quer `fullName`.
// Evite
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
Isso renderiza com o valor velho, roda o effect, chama `setState` e renderiza de novo. Dois renders para algo que é uma conta.
// Prefira
const fullName = `${firstName} ${lastName}`;
O mesmo vale para filtrar ou ordenar uma lista que já está em props ou em estado: calcule durante o render. Se você pode calcular um valor a partir de props e estado que já existem, ele não deve ser um estado novo.
2. Cálculo caro: useMemo, não effect + state
Se o cálculo é realmente pesado, memorize o resultado em vez de guardá-lo em estado.
const visibleTodos = useMemo(
() => getFilteredTodos(todos, filter),
[todos, filter]
);
Meça antes de memorizar. Na maioria das telas o cálculo é barato e o `useMemo` só adiciona ruído. Se o projeto usa o React Compiler, ele já memoriza boa parte disso por você.
3. Resetar estado quando uma prop muda: use key
Quer zerar o formulário de comentário quando o usuário troca de perfil?
// Evite
useEffect(() => {
setComment('');
}, [userId]);
O componente renderiza uma vez com o comentário antigo, depois limpa. Em vez disso, diga ao React que é outro componente:
<Profile userId={userId} key={userId} />
Quando a `key` muda, o React descarta o componente e todo o estado dele, e monta um novo do zero.
4. Reagir a uma ação do usuário: use o event handler
Se algo acontece porque o usuário clicou, o código vai no handler do clique.
// Evite
useEffect(() => {
if (submitted) {
post('/api/register', { email });
}
}, [submitted]);
// Prefira
function handleSubmit() {
post('/api/register', { email });
}
O handler sabe exatamente o que causou a ação. O effect só vê que um estado mudou e tem que adivinhar o motivo. Isso inclui avisar o componente pai: chame `onChange` dentro do handler, no mesmo lugar em que você chama o `setState`, em vez de disparar a prop num effect depois.
5. Buscar dados: deixe uma biblioteca ou o framework cuidar
Buscar dados dentro de um effect funciona, mas você assume sozinho os problemas: resposta que chega fora de ordem, cache, estado de carregamento e erro, dados duplicados em duas telas.
useEffect(() => {
let ignore = false;
fetchResults(query).then((json) => {
if (!ignore) setResults(json);
});
return () => {
ignore = true;
};
}, [query]);
O `ignore` evita que uma resposta antiga sobrescreva a nova quando `query` muda rápido. Funciona, mas é código que você vai repetir em todo componente. Bibliotecas como TanStack Query, ou os recursos de dados do seu framework, resolvem isso uma vez, com cache e revalidação. Se você não usa nenhuma, o effect acima é a forma correta de escrever, só saiba que é o mínimo.
6. Assinar um store externo: useSyncExternalStore
Para ler algo que vive fora do React e muda sozinho, como `navigator.onLine` ou um store próprio, o hook feito para isso é o `useSyncExternalStore`.
function subscribe(callback: () => void) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function useOnlineStatus() {
return useSyncExternalStore(
subscribe,
() => navigator.onLine,
() => true // valor no servidor
);
}
O React garante que a tela nunca mostre dois valores diferentes do store no mesmo render, algo que o combo effect + state não garante.
Quando o effect é a escolha certa
Nada disso significa banir o `useEffect`. Ele é a ferramenta certa quando você sincroniza com algo de fora:
- conexão de WebSocket ou outra assinatura que precisa abrir e fechar com o componente; - um listener no `window` ou `document`; - integrar uma biblioteca que não é React (mapa, player de vídeo, gráfico) a um elemento do DOM; - registrar que uma tela foi exibida, como evento de analytics.
Nesses casos, escreva sempre a função de limpeza. Ela é o que impede listener duplicado e conexão vazando.
O Strict Mode é seu amigo
Em desenvolvimento, o Strict Mode monta, desmonta e monta o componente de novo, rodando seus effects duas vezes. Parece bug, mas é um teste de graça: se o effect quebra quando roda duas vezes, falta a limpeza. Se ele funciona duas vezes, funciona em produção.
Checklist antes de escrever um useEffect
- Dá para calcular isso no render? → calcule no render - É um cálculo caro? → `useMemo`, depois de medir - Preciso zerar estado quando uma prop muda? → `key` - Isso acontece por causa de um clique ou envio? → event handler - É busca de dados? → biblioteca ou framework - É um store externo? → `useSyncExternalStore` - É sincronização com o mundo de fora? → `useEffect`, com cleanup
Fechando
O effect não é vilão. O problema é usá-lo como lugar padrão para qualquer lógica que "reage a mudança". Quando você troca effect por render, key e handler, o código fica menor, tem menos render extra e fica mais fácil de seguir.
Da próxima vez que for escrever um useEffect, faz uma pergunta antes: isso fala com algo fora do React? Se não falar, provavelmente cabe no render.
No meu painel, o total virou uma linha de código e o número velho parou de piscar.
Para ler mais
- [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect), documentação oficial do React - [Synchronizing with Effects](https://react.dev/learn/synchronizing-with-effects) - [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore) - [useMemo](https://react.dev/reference/react/useMemo)