Um componente de chat reconecta o WebSocket toda vez que você digita uma letra em um campo de busca que não tem nada a ver com o chat. Outro componente re-renderiza em loop até a aba congelar. Um terceiro registra seu useEffect duas vezes ao montar, mas só em desenvolvimento. Nenhum desses é um bug do React. Os dois primeiros são o array de dependências fazendo exatamente o que você pediu, e o terceiro é o Strict Mode verificando se o seu efeito sabe se limpar depois de si mesmo.
Quando usar useEffect em um componente React
useEffect sincroniza um componente com algo que vive fora do React: uma conexão com um servidor, uma API do navegador, um timer, o DOM diretamente, um widget de terceiros. O React renderiza seu JSX, aplica no DOM e só então executa seus efeitos. Quando o render não foi causado por uma interação como um clique, o React normalmente deixa o navegador pintar a nova tela primeiro.
Então um efeito é o lugar errado para calcular um valor destinado ao render. Se fullName pode ser derivado de firstName e lastName, escreva isso durante o render:
| |
Não um efeito que chama setFullName depois do fato. A versão com efeito renderiza uma vez com o valor desatualizado, agenda uma atualização de estado, depois renderiza de novo. Esse passo extra não traz nada. Use useEffect quando o componente precisa fazer algo porque foi exibido, não para calcular algo que poderia calcular diretamente.
Como funciona o array de dependências do useEffect
O segundo argumento do useEffect aparece em três formas, e cada uma diz ao React algo diferente sobre quando executar a função de novo.
| |
Para um array preenchido, o React compara cada item com seu valor no render anterior usando Object.is. É a mesma verificação de ===, exceto que NaN é igual a si mesmo e 0 e -0 são diferentes. Se cada dependência for idêntica à última vez, o React pula o efeito. Se uma for diferente, ele executa de novo. Não existe correspondência parcial, nem forma de dizer ao React “execute de novo só quando esta dependência específica mudar”. Essa lógica precisa ficar dentro do efeito.
Isso também significa que você não pode deixar um valor de fora do array para impedir que o efeito seja executado de novo. O array deve listar todo valor reativo que o corpo da função lê: estado, props e qualquer coisa derivada deles. Tire um valor do array mas continue lendo ele, e o efeito roda com uma cópia desatualizada. A regra react-hooks/exhaustive-deps do eslint-plugin-react-hooks existe justamente para pegar esse tipo de desalinhamento. Trate seus avisos como erro de build, não os silencie com um comentário de desativação.
O que a função de cleanup do useEffect faz
Um efeito pode retornar uma função. O React trata esse valor de retorno como o cleanup de tudo que o efeito iniciou:
| |
O React chama o cleanup em duas situações: pouco antes de o efeito executar de novo porque uma dependência mudou, e quando o componente desmonta. Ele sempre limpa o efeito anterior antes de iniciar o próximo. Conecta, desconecta, conecta, desconecta. Nunca duas conexões abertas ao mesmo tempo.
Qualquer coisa que um efeito inicie e que precise de uma parada explícita vai atrás de uma função de cleanup: subscribe precisa de unsubscribe, addEventListener precisa de removeEventListener, setInterval precisa de clearInterval. O mesmo padrão resolve um bug mais sutil de busca de dados, em que uma resposta antiga chega depois de uma mais nova:
| |
Se personId muda antes de a primeira requisição se resolver, o cleanup define ignore como true para aquela requisição, então seu callback .then() não faz nada em vez de sobrescrever um estado mais recente com dados mais antigos.
Por que o useEffect executa duas vezes em desenvolvimento
Em desenvolvimento, sob o StrictMode, o React executa o setup, depois o cleanup, depois o setup de novo, imediatamente após a primeira montagem. Em produção o setup executa uma única vez. O React faz isso de propósito: seu componente vai ser desmontado e remontado no uso real (navegar para fora de uma rota e voltar, por exemplo), e o Strict Mode força isso a acontecer primeiro na sua máquina, onde um cleanup ausente aparece como um bug visível.
O erro é remendar o sintoma:
| |
O guard bloqueia a segunda chamada de connect(), mas ainda não há cleanup, então a conexão fica aberta depois que o componente desmonta. Se remontar quebra um efeito, falta cleanup ao efeito. Escreva a função de cleanup em vez de se defender da segunda execução.
Erros comuns com dependências do useEffect
Um objeto ou função literal recriado a cada render. Dois objetos com conteúdo idêntico não são o mesmo objeto:
| |
Coloque um objeto assim em um array de dependências e o efeito vê um valor “novo” a cada render, tenha algo relevante mudado ou não:
| |
Construa o objeto dentro do efeito em vez disso, e dependa dos valores primitivos de que ele é feito:
| |
Ler o estado só para calcular o próximo estado. Um interval que lê count para incrementá-lo obriga o efeito a depender de count, o que recria o interval em cada tick:
| |
Uma atualização funcional remove essa leitura, e a dependência vai junto:
| |
Um efeito que faz dois trabalhos sem relação. Imagine um efeito que busca as cidades de um país e os bairros de uma cidade. Com ambos na mesma função e o mesmo array de dependências, trocar a cidade também refaz a busca das cidades. Separe-os: um efeito para cada parte independente de sincronização, cada um com seu próprio array.
Quando você não precisa de useEffect
Dois padrões parecem precisar de um efeito e não precisam.
Resetar o estado quando uma prop muda. Limpar o estado local de um formulário quando userId muda é tentador escrever como um efeito que observa userId. Funciona, mas o componente renderiza uma vez com o estado antigo antes de o reset chegar. Passe o valor que muda como key do componente, em vez disso, e o React trata como uma instância nova, sem nenhum efeito envolvido:
| |
Algo que acontece por causa de um clique, não de um render. Uma notificação de “adicionado ao carrinho” que observa product.isInCart em um efeito dispara de novo a cada recarregamento de página em que o item já está no carrinho, porque o efeito não consegue distinguir “acabou de ser adicionado” de “já estava lá”. A notificação pertence ao handler do clique que adiciona o item, onde executa só quando o usuário clica:
| |
Se algo deve acontecer porque o usuário fez algo, coloque no event handler desse algo. useEffect é para o que precisa ficar sincronizado o tempo todo que o componente está na tela, independente do que o usuário faz. Sincronizar uma busca com o ciclo de vida do componente é esse tipo de problema (o lado da requisição é tratado em tratamento de erros da Fetch API), assim como desconectar um observer ao desmontar, como na Intersection Observer API.
Na próxima vez que um efeito se comportar mal, deixe o array em paz no início. Liste o que o corpo da função lê, decida se cada leitura realmente precisa ser reativa, e verifique se o código funciona sem nenhum efeito. Corrija o corpo, e o array correto vem disso. Se a ordem dos callbacks assíncronos em torno de um render é o que confunde você, leia como funcionam async/await e o event loop, que cobre a call stack e a fila de microtasks.