useEffect: os casos em que não precisa de nenhum
Há um padrão que aparece em quase todos os projetos React com mais de um ano: um useEffect que observa um valor e escreve outro. Funciona — e é quase sempre a forma mais cara e mais frágil de fazer aquilo que se queria.
Uma pergunta só resolve a maior parte destes casos: isto tem de acontecer porque o componente apareceu no ecrã, ou porque alguém fez alguma coisa? Se for a segunda, não é um efeito.
1. Estado derivado: calcule durante o render
// Não
const [nome, setNome] = useState('');
const [apelido, setApelido] = useState('');
const [completo, setCompleto] = useState('');
useEffect(() => {
setCompleto(nome + ' ' + apelido);
}, [nome, apelido]);
// Sim
const completo = nome + ' ' + apelido;O efeito custa um render inteiro a mais: o React desenha com o nome novo e o completo velho, corre o efeito, muda o estado, e desenha outra vez. Há um instante — curto, mas real — em que o ecrã mostra dois valores que não combinam um com o outro.
E depois há o resto: mais três linhas, mais um estado para manter sincronizado, e mais uma lista de dependências para alguém esquecer.
2. Uma lista filtrada é o mesmo caso
// Não
const [visiveis, setVisiveis] = useState([]);
useEffect(() => {
setVisiveis(artigos.filter((a) => a.etiqueta === etiqueta));
}, [artigos, etiqueta]);
// Sim
const visiveis = artigos.filter((a) => a.etiqueta === etiqueta);«Mas assim corre em todos os renders.» Corre — e filtrar mil objetos demora menos de um milissegundo. Se um dia for mesmo caro, embrulha-se num useMemo depois de medir, e é uma linha. Guardar o resultado em estado não é uma linha: é um estado a mais, que pode ficar dessincronizado do que lhe deu origem.
3. O que acontece por causa de um clique pertence ao clique
// Não: o efeito tem de adivinhar porque é que o carrinho mudou
useEffect(() => {
if (produtos.length > 0) mostrarAviso('Produto adicionado');
}, [produtos]);
// Sim
function aoAdicionar(produto) {
setProdutos([...produtos, produto]);
mostrarAviso('Produto adicionado');
}Na primeira versão o aviso aparece também quando o carrinho é carregado da sessão anterior, e quando se remove um produto e ainda sobra outro. O efeito não sabe porquê; só sabe que o valor mudou. Quem sabe porquê é o handler.
4. Repor o estado quando uma prop muda: use a key
// Não
function Perfil({ utilizadorId }) {
const [rascunho, setRascunho] = useState('');
useEffect(() => {
setRascunho('');
}, [utilizadorId]);
}
// Sim: quem trata disto é quem usa o componente
<Perfil key={utilizadorId} utilizadorId={utilizadorId} />Mudar a key diz ao React que aquele já não é o mesmo componente: ele deita o estado fora e monta um novo. Sem efeito, e sem aquele render intermédio em que o rascunho da pessoa anterior ainda está à vista no ecrã da seguinte.
5. Então para que servem os efeitos?
Para uma coisa só: sincronizar o React com o que está fora do React. Uma subscrição, um addEventListener, um temporizador, uma biblioteca que mexe no DOM à maneira dela, um pedido ao servidor.
useEffect(() => {
const ligacao = ligarAoCanal(salaId);
ligacao.abrir();
// Sem esta linha, fica uma ligação aberta por cada visita
return () => ligacao.fechar();
}, [salaId]);A limpeza não é opcional. Em desenvolvimento, o StrictMode monta, desmonta e volta a montar cada componente de propósito, precisamente para que um efeito sem limpeza dê nas vistas antes de chegar a produção. Quando alguém diz que «o React faz o efeito correr duas vezes», é quase sempre isto — e o problema não era o React.
6. Buscar dados: se tiver mesmo de ser num efeito, faça-o bem
useEffect(() => {
let cancelado = false;
fetch('/api/artigos/' + id)
.then((r) => r.json())
.then((dados) => {
if (!cancelado) setArtigo(dados);
});
return () => {
cancelado = true;
};
}, [id]);Sem aquela variável, duas mudanças rápidas de id podem acabar com a resposta do primeiro pedido a chegar depois da do segundo — e o ecrã fica a mostrar o artigo errado, sem erro nenhum em lado nenhum. É a falha mais difícil de reproduzir e das mais fáceis de evitar.
Dito isto: um efeito não guarda nada em cache, não junta dois pedidos iguais num só, e obriga a esperar que o componente apareça no ecrã antes de sequer começar a pedir. Quem tem um framework por baixo, ou uma biblioteca de dados, quase de certeza tem maneira melhor de fazer isto.
7. Ler um valor sem depender dele
Sobra um caso legítimo e chato: o efeito precisa de ler um valor, mas não deve voltar a correr quando esse valor muda. Durante anos a saída foi mentir na lista de dependências e viver com o aviso na consola. Desde o 19.2 há o useEffectEvent:
const registarVisita = useEffectEvent(() => {
// Lê sempre o tema atual, seja ele qual for
analitica.registar(salaId, tema);
});
useEffect(() => {
registarVisita();
}, [salaId]); // mas só volta a correr quando a sala mudaComo é recente, confirme a versão que tem instalada antes de contar com ele.
O resumo
Antes de escrever um useEffect, duas perguntas. Isto consegue-se calcular a partir do que já tenho? Então calcule, e não guarde. Isto acontece por causa de uma ação de alguém? Então ponha-o onde essa ação já está tratada.
O que sobrar das duas — e sobra pouco — é aquilo para que os efeitos existem.