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:

1
const fullName = firstName + ' ' + lastName;

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
useEffect(() => {
  // executa depois de cada render
});

useEffect(() => {
  // executa uma vez, depois que o componente monta pela primeira vez
}, []);

useEffect(() => {
  // executa depois de montar, e de novo sempre que roomId muda
}, [roomId]);

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:

1
2
3
4
5
6
7
8
useEffect(() => {
  const connection = createConnection(roomId);
  connection.connect();

  return () => {
    connection.disconnect();
  };
}, [roomId]);

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
useEffect(() => {
  let ignore = false;

  fetchBio(personId).then((result) => {
    if (!ignore) setBio(result);
  });

  return () => {
    ignore = true;
  };
}, [personId]);

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:

1
2
3
4
5
6
7
8
9
// Parece corrigir a "conexão duplicada" — não corrige.
const connectionRef = useRef(null);

useEffect(() => {
  if (!connectionRef.current) {
    connectionRef.current = createConnection(roomId);
    connectionRef.current.connect();
  }
}, [roomId]);

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:

1
2
3
const options1 = { serverUrl: 'wss://chat.example.com', roomId: 'general' };
const options2 = { serverUrl: 'wss://chat.example.com', roomId: 'general' };
Object.is(options1, options2); // false

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:

1
2
3
4
5
6
7
8
9
function ChatRoom({ roomId }) {
  const options = { serverUrl: 'wss://chat.example.com', roomId }; // objeto novo a cada render

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [options]); // reconecta a cada render, até os que não têm relação
}

Construa o objeto dentro do efeito em vez disso, e dependa dos valores primitivos de que ele é feito:

1
2
3
4
5
6
7
8
function ChatRoom({ roomId }) {
  useEffect(() => {
    const options = { serverUrl: 'wss://chat.example.com', roomId };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // uma string é comparada por valor
}

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:

1
2
3
4
5
6
useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1); // lê count -> count precisa ser dependência
  }, 1000);
  return () => clearInterval(id);
}, [count]); // o interval é destruído e recriado a cada segundo

Uma atualização funcional remove essa leitura, e a dependência vai junto:

1
2
3
4
5
6
useEffect(() => {
  const id = setInterval(() => {
    setCount((c) => c + 1); // sem leitura externa
  }, 1000);
  return () => clearInterval(id);
}, []); // executa uma vez

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:

1
<Profile userId={userId} key={userId} />

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:

1
2
3
4
function handleBuyClick() {
  addToCart(product);
  showNotification(`${product.name} adicionado ao carrinho`);
}

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.

Artigos relacionados