Removendo Dependências de Efeitos

Quando você escreve um efeito, o linter verifica se você incluiu todos os valores reativos (como props e state) que o efeito lê na lista de dependências do seu efeito. Isso garante que seu efeito permaneça sincronizado com as últimas propriedades e o estado de seu componente. Dependências desnecessárias podem fazer com que seu Efeito seja executado com muita frequência ou até mesmo criar um loop infinito. Siga este guia para revisar e remover dependências desnecessárias de seus efeitos.

Você aprenderá

  • Como corrigir loops de dependência de efeito infinito
  • O que fazer quando você quiser remover uma dependência
  • Como ler um valor de seu Efeito sem “reagir” a ele
  • Como e por que evitar dependências de objetos e funções
  • Por que suprimir o linter de dependência é perigoso e o que fazer em vez disso

As Dependências devem corresponder ao código

Quando você escreve um Efeito, o primeiro passo é especifica como iniciar e parar o que o seu efeito faz:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
// ...
}

Portanto, se você deixar as dependências do Efeito vazias ([]), o linter sugerirá as dependências corretas:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // <-- Corrija o erro aqui!
  return <h1>Bem-vindo(a) à sala {roomId}!</h1>;
}

export default function App() {
  const [roomId, setRoomId] = useState('geral');
  return (
    <>
      <label>
         Escolha a sala de bate-papo:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="geral">geral</option>
          <option value="viagem">viagem</option>
          <option value="música">música</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

Preencha-os de acordo com o que o linter indica:

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Todas as dependências declaradas
// ...
}

Os Efeitos “reagem” a valores reativos. Como roomId é um valor reativo (ele pode mudar devido a uma re-renderização), o linter verifica se você o especificou como uma dependência. Se roomId receber um valor diferente, o React irá re-sincronizar o seu Efeito. Isso garante que o chat permaneça conectado à sala selecionada e “reaja” ao menu suspenso:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);
  return <h1>Bem-vindo(a) à sala {roomId}!</h1>;
}

export default function App() {
  const [roomId, setRoomId] = useState('geral');
  return (
    <>
      <label>
        Escolha a sala de bate-papo:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="geral">geral</option>
          <option value="viagem">viagem</option>
          <option value="música">música</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

Para remover uma dependência, prove que ela não é uma dependência

Observe que você não pode “escolher” as dependências do seu Efeito. Todo valor reativo usado pelo código do seu Efeito deve ser declarado na sua lista de dependências. A lista de dependências é estabelecida pelo código ao redor:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) { // Este é um valor reativo
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Esse efeito indica que o valor reativo
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Portanto, você deve especificar esse valor reativo como uma dependência do seu Efeito
// ...
}

Valores reativos incluem props e todas as variáveis e funções declaradas diretamente dentro do seu componente. Como roomId é um valor reativo, você não pode removê-lo da lista de dependências. O linter não permitirá isso:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect tem uma dependência ausente: 'roomId'
// ...
}

E o linter estaria correto! Como o roomId pode mudar ao longo do tempo, removê-lo da lista de dependências poderia introduzir um bug no seu código.

Para remover uma dependência, “prove” ao linter que ela não precisa ser uma dependência. Por exemplo, você pode mover roomId para fora do seu componente para demonstrar que ele não é reativo e não mudará em re-renderizações:

const serverUrl = 'https://localhost:1234';
const roomId = 'música'; // Não se trata mais de um valor reativo

function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ Todas as dependências declaradas
// ...
}

Agora que roomId não é um valor reativo (e não pode ser alterado em uma nova renderização), ele não precisa ser uma dependência:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';
const roomId = 'música';

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []);
  return <h1>Bem-vindo(a) à sala {roomId}!</h1>;
}

É por isso que agora você pode especificar uma lista de dependências vazia ([]). Seu Efeito não depende mais de nenhum valor reativo, portanto, não precisa ser reexecutado quando qualquer propriedade ou estado do componente for alterado.

Para alterar as dependências, altere o código

Talvez você tenha notado um padrão em seu fluxo de trabalho:

  1. Primeiro, você altera o código do seu Efeito ou a forma como seus valores reativos são declarados.
  2. Em seguida, você segue as orientações do linter e ajusta as dependências para corresponder ao código que você alterou.
  3. Se não estiver satisfeito com a lista de dependências, você volta ao primeiro passo (e altera o código novamente).

A última parte é importante. Se você deseja alterar as dependências, modifique o código ao redor primeiro. Você pode considerar a lista de dependências como uma lista de todos os valores reativos usados pelo código do seu Efeito. Você não escolhe o que colocar nessa lista. A lista descreve o seu código. Para alterar a lista de dependências, altere o código.

Isso pode parecer como resolver uma equação. Você pode começar com um objetivo (por exemplo, remover uma dependência) e precisa “encontrar” o código que corresponda a esse objetivo. Nem todo mundo acha divertido resolver equações, e a mesma coisa pode ser dita sobre escrever efeitos! Felizmente, há uma lista de receitas comuns que você pode experimentar abaixo.

Pitfall

Se você tiver uma base de código existente, poderá ter alguns efeitos que suprimem o linter dessa forma:

useEffect(() => {
// ...
// 🔴 Evite ignorar o linter assim:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

Quando as dependências não correspondem ao código, há um risco muito alto de introduzir bugs. Ignorar o linter faz com que você “enganar” o React sobre os valores dos quais seu efeito depende.

Em vez disso, utilize as técnicas abaixo.

Deep Dive

Por que ignorar o linter de dependências é tão perigoso?

Ignorar o linter pode resultar em bugs complexos e não intuitivos, que são difíceis de identificar e corrigir. Veja um exemplo:

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  function onTick() {
    setCount(count + increment);
  }

  useEffect(() => {
    const id = setInterval(onTick, 1000);
    return () => clearInterval(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <h1>
        Contador: {count}
        <button onClick={() => setCount(0)}>Reiniciar</button>
      </h1>
      <hr />
      <p>
        A cada segundo, incremente:
        <button disabled={increment === 0} onClick={() => {
          setIncrement(i => i - 1);
        }}></button>
        <b>{increment}</b>
        <button onClick={() => {
          setIncrement(i => i + 1);
        }}>+</button>
      </p>
    </>
  );
}

Digamos que você queira executar o efeito “somente na montagem”. Você leu que uma lista de dependências vazia ([]) faz isso, então decidiu ignorar o linter e especificou [] como as dependências.

Este contador deveria ser incrementado a cada segundo pelo valor configurável com os dois botões. No entanto, como você “enganou” o React, dizendo que esse Efeito não depende de nada, o React continua usando a função onTick do render inicial. Durante esse render, o count era 0 e o increment era 1. É por isso que onTick daquele render sempre chama setCount(0 + 1) a cada segundo, e você sempre vê o valor 1. Bugs como este são mais difíceis de corrigir quando estão espalhados por vários componentes.

Sempre há uma solução melhor do que ignorar o linter! Para corrigir esse código, você precisa adicionar onTick à lista de dependências. (Para garantir que o intervalo seja configurado apenas uma vez, faça onTick um Evento de Efeito)

Recomendamos tratar o erro de dependência do lint como um erro de compilação. Se você não ignorá-lo, nunca encontrará erros como este. O restante desta página documenta as alternativas para esse e outros casos.

Removendo dependências desnecessárias

Toda vez que você ajustar as dependências do Efeito para refletir o código, examine a lista de dependências. Faz sentido que o Efeito seja executado novamente quando alguma dessas dependências for alterada? Às vezes, a resposta é “não”:

  • Você pode querer reexecutar partes diferentes do seu Efeito sob condições distintas.
  • Pode ser necessário ler apenas o valor mais recente de alguma dependência em vez de “reagir” às suas alterações.
  • Uma dependência pode mudar com muita frequência não intencionalmente porque é um objeto ou uma função.

Para encontrar a solução certa, você precisará responder a algumas perguntas sobre o seu Efeito. Vamos examiná-las.

Esse código deve ser movido para um manipulador de eventos?

A primeira coisa que você deve pensar é se esse código deve ser um Efeito.

Imagine um formulário. Ao enviar, você define a variável de estado submitted como true. Você precisa enviar uma solicitação POST e mostrar uma notificação. Você colocou essa lógica dentro de um Efeito que “reage” ao fato de submitted ser true:

function Form() {
const [submitted, setSubmitted] = useState(false);

useEffect(() => {
if (submitted) {
// 🔴 Evite: Lógica específica do evento dentro de um Efeito
post('/api/register');
showNotification('Bem-sucedido registrado!');
}
}, [submitted]);

function handleSubmit() {
setSubmitted(true);
}

// ...
}

Posteriormente, você deseja estilizar a mensagem de notificação de acordo com o tema atual, portanto, você lê o tema atual. Como o theme é declarado no corpo do componente, ele é um valor reativo, portanto, você o adiciona como uma dependência:

function Form() {
const [submitted, setSubmitted] = useState(false);
const theme = useContext(ThemeContext);

useEffect(() => {
if (submitted) {
// 🔴 Evite: Lógica específica do evento dentro de um Efeito
post('/api/register');
showNotification('Bem-sucedido registrado!', theme);
}
}, [submitted, theme]); // ✅ Todas as dependências declaradas

function handleSubmit() {
setSubmitted(true);
}

// ...
}

Ao fazer isso, você introduziu um bug. Imagine que você envie o formulário primeiro e depois alterne entre os temas Escuro e Claro. O theme mudará, o Efeito será executado novamente e, portanto, exibirá a mesma notificação novamente!

O problema aqui é que isso não deveria ser um efeito em primeiro lugar. Você deseja enviar essa solicitação POST e mostrar a notificação em resposta ao envio do formulário, que é uma interação específica. Para executar algum código em resposta a uma interação específica, coloque essa lógica diretamente no manipulador de eventos correspondente:

function Form() {
const theme = useContext(ThemeContext);

function handleSubmit() {
// ✅ Bom: A lógica específica do evento é chamada pelos manipuladores de eventos
post('/api/register');
showNotification('Bem-sucedido registrado!', theme);
}

// ...
}

Agora que o código está em um manipulador de eventos, ele não é reativo, portanto, só será executado quando o usuário enviar o formulário. Leia mais sobre escolhendo entre manipuladores de eventos e efeitos e como excluir efeitos desnecessários

Seu efeito está fazendo várias coisas não relacionadas?

A próxima pergunta que você deve fazer a si mesmo é se o seu Efeito está fazendo várias coisas não relacionadas.

Imagine que você está criando um formulário de envio em que o usuário precisa escolher a cidade e a região. Você obtém a lista de cities do servidor de acordo com o country selecionado para mostrá-las em um menu suspenso:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);

useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ Todas as dependências declaradas

// ...

Esse é um bom exemplo de obtenção de dados em um Efeito Você está sincronizando o estado das cities com a rede de acordo com a propriedade country. Não é possível fazer isso em um manipulador de eventos porque você precisa buscar os dados assim que o ShippingForm for exibido e sempre que o country for alterado (independentemente da interação que o causar).

Agora, digamos que você está adicionando uma segunda caixa de seleção para áreas da cidade, que deve buscar as areas da city selecionada no momento. Você pode começar adicionando uma segunda chamada fetch para obter a lista de áreas dentro do mesmo Efeito:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);

useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
// 🔴 Evite: Um único efeito sincroniza dois processos independentes
if (city) {
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
}
return () => {
ignore = true;
};
}, [country, city]); // ✅ Todas as dependências declaradas

// ...

Entretanto, como o Efeito agora usa a variável de estado city, você teve que adicionar city à lista de dependências. Isso, por sua vez, introduziu um problema: quando o usuário selecionar uma cidade diferente, o Efeito será executado novamente e chamará fetchCities(country). Como resultado, você estará recuperando desnecessariamente a lista de cidades várias vezes.

O problema com esse código é que você está sincronizando duas coisas diferentes e não relacionadas:

  1. Você deseja sincronizar o estado cities com a rede com base na propriedade country.
  2. Você deseja sincronizar o estado areas com a rede com base no estado city.

Divida a lógica em dois Efeitos, cada um dos quais reage à propriedade com a qual precisa se sincronizar:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ Todas as dependências declaradas

const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]); // ✅ Todas as dependências declaradas

// ...

Agora, o primeiro efeito só é executado novamente se o country altera, enquanto o segundo efeito é executado novamente quando a city altera. Você os separou por finalidade: duas coisas diferentes são sincronizadas por dois efeitos separados. Dois efeitos separados têm duas listas de dependências separadas, de modo que não acionam um ao outro de forma não intencional.

O código final é mais longo que o original, mas a divisão desses Efeitos ainda está correta. Cada efeito deve representar um processo de sincronização independente Neste exemplo, a exclusão de um efeito não quebra a lógica do outro efeito. Isso significa que eles sincronizam coisas diferentes e é bom separá-los. Se estiver preocupado com a duplicação, você pode melhorar esse código extraindo a lógica repetitiva em um Hook personalizado

Você está lendo algum estado para calcular o próximo estado?

Este Efeito atualiza a variável de estado messages com uma array recém-criada sempre que chega uma nova mensagem:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
// ...

Ele usa a variável messages para criar um novo array começando com todas as mensagens existentes e adicionando a nova mensagem no final. Entretanto, como messages é um valor reativo lido por um Efeito, ele deve ser uma dependência:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId, messages]); // ✅ Todas as dependências declaradas
// ...

E tornar messages uma dependência introduz um problema.

Toda vez que você recebe uma mensagem, setMessages() faz com que o componente seja renderizado novamente com um novo array messages que inclui a mensagem recebida. Entretanto, como esse Efeito agora depende de messages, isso também ressincronizará o Efeito. Portanto, cada nova mensagem fará com que o chat se reconecte. O usuário não gostaria disso!

Para corrigir o problema, não leia messages dentro do Efeito. Em vez disso, passe uma função de atualização para setMessages:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId]); // ✅ Todas as dependências declaradas
// ...

Observe como seu Efeito não lê a variável messages agora. Você só precisa passar uma função atualizadora como msgs => [...msgs, receivedMessage]. O React coloca sua função atualizadora em uma fila e fornecerá o argumento msgs a ela durante a próxima renderização. É por isso que o efeito em si não precisa mais depender de messages. Como resultado dessa correção, o recebimento de uma mensagem de chat não fará mais com que o chat seja reconectado.

Você deseja ler um valor sem “reagir” às suas alterações?

Under Construction

Esta seção descreve uma API experimental que ainda não foi lançada em uma versão estável do React.

Suponha que você queira tocar um som quando o usuário receber uma nova mensagem, a menos que isMuted seja true:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
// ...

Como seu Efeito agora usa isMuted em seu código, você precisa adicioná-lo às dependências:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
return () => connection.disconnect();
}, [roomId, isMuted]); // ✅ Todas as dependências declaradas
// ...

O problema é que toda vez que isMuted é alterado (por exemplo, quando o usuário pressiona o botão de alternância Silenciado), o Efeito é ressincronizado e se reconecta ao chat. Essa não é a experiência de usuário desejada! (Neste exemplo, nem mesmo a desativação do linter funcionaria - se você fizer isso, o isMuted ficará “preso” ao seu valor antigo).

Para resolver esse problema, você precisa extrair do Efeito a lógica que não deve ser reativa. Você não quer que esse efeito “reaja” às alterações em isMuted. Mova essa parte não reativa da lógica para um Evento de Efeito:

import { useState, useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

const onMessage = useEffectEvent(receivedMessage => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ Todas as dependências declaradas
// ...

Os Eventos de Efeito permitem que você divida um Efeito em partes reativas (que devem “reagir” a valores reativos como roomId e suas alterações) e partes não reativas (que apenas leem seus valores mais recentes, como onMessageisMuted). Agora que você lê isMuted dentro de um Evento de Efeito, ele não precisa ser uma dependência do seu Efeito. Como resultado, o chat não se reconectará quando você ativar e desativar a configuração “Muted”, resolvendo o problema original!

Envolvimento de um manipulador de eventos a partir dos props

Você pode se deparar com um problema semelhante quando seu componente recebe um manipulador de eventos como uma propriedade:

function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onReceiveMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId, onReceiveMessage]); // ✅ Todas as dependências declaradas
// ...

Suponha que o componente pai passe uma função onReceiveMessage diferente a cada renderização:

<ChatRoom
roomId={roomId}
onReceiveMessage={receivedMessage => {
// ...
}}
/>

Como o onReceiveMessage é uma dependência, isso faria com que o Efeito fosse ressincronizado após cada nova renderização do pai. Isso faria com que ele se reconectasse ao chat. Para resolver isso, envolva a chamada em um Evento de Efeito:

function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);

const onMessage = useEffectEvent(receivedMessage => {
onReceiveMessage(receivedMessage);
});

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ Todas as dependências declaradas
// ...

Os Eventos de Efeitos não são reativos, portanto, você não precisa especificá-los como dependências. Como resultado, o bate-papo não será mais reconectado, mesmo que o componente pai passe uma função que seja diferente a cada nova renderização.

Separação de código reativo e não reativo

Neste exemplo, você deseja registrar uma visita sempre que o roomId for alterado. Além disso, você quer incluir o notificationCount atual em cada registro, mas sem que uma alteração em notificationCount dispare um novo evento de registro.

A solução, mais uma vez, é separar o código não reativo em um Evento de Efeito:

function Chat({ roomId, notificationCount }) {
const onVisit = useEffectEvent(visitedRoomId => {
logVisit(visitedRoomId, notificationCount);
});

useEffect(() => {
onVisit(roomId);
}, [roomId]); // ✅ Todas as dependências declaradas
// ...
}

Você deseja que sua lógica seja reativa com relação a roomId, portanto, você lê roomId dentro do seu Efeito. No entanto, você não quer que uma alteração em notificationCount registre uma visita extra, então você lê notificationCount dentro do Evento de Efeito. Saiba mais sobre como ler as últimas props e o estado dos efeitos usando Efeito Events

Algum valor reativo muda involuntariamente?

Às vezes, você quer que seu Efeito “reaja” a um determinado valor, mas esse valor muda com mais frequência do que você gostaria e pode não refletir nenhuma mudança real da perspectiva do usuário. Por exemplo, digamos que você crie um objeto options no corpo do seu componente e, em seguida, leia esse objeto de dentro do seu Efeito:

function ChatRoom({ roomId }) {
// ...
const options = {
serverUrl: serverUrl,
roomId: roomId
};

useEffect(() => {
const connection = createConnection(options);
connection.connect();
// ...

Esse objeto é declarado no corpo do componente, portanto, é um valor reativo. Quando você lê um valor reativo como esse dentro de um Efeito, você o declara como uma dependência. Isso garante que seu efeito “reaja” a suas alterações:

// ...
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ Todas as dependências declaradas
// ...

É importante declará-lo como uma dependência! Isso garante, por exemplo, que se o roomId for alterado, seu Efeito se conectará novamente ao chat com as novas options. No entanto, também há um problema com o código acima. Para ver isso, tente digitar na entrada da caixa de areia abaixo e observe o que acontece no console:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  // Desative temporariamente a linter para demonstrar o problema
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [options]);

  return (
    <>
      <h1>Bem-vindo(a) à sala {roomId}!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('geral');
  return (
    <>
      <label>
        Escolha a sala de bate-papo:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="geral">geral</option>
          <option value="viagem">viagem</option>
          <option value="música">música</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

No exemplo acima, a entrada de texto apenas atualiza a variável de estado message. Do ponto de vista do usuário, isso não deveria afetar a conexão do chat. No entanto, toda vez que você atualiza a message, o seu componente é re-renderizado. Quando o componente é re-renderizado, o código dentro dele é executado novamente desde o início.

Um novo objeto options é criado do zero a cada re-renderização do componente ChatRoom. O React percebe que o objeto options é diferente do objeto options criado durante a renderização anterior. É por isso que ele re-sincroniza o seu Efeito (que depende de options), e o chat se reconecta conforme você digita.

Esse problema afeta apenas objetos e funções. Em JavaScript, cada objeto e função recém-criados são considerados distintos de todos os outros. Não importa se o conteúdo dentro deles pode ser o mesmo!

// Durante a primeira renderização
const options1 = { serverUrl: 'https://localhost:1234', roomId: 'música' };

// Durante a próxima renderização
const options2 = { serverUrl: 'https://localhost:1234', roomId: 'música' };

// Esses são dois objetos diferentes!
console.log(Object.is(options1, options2)); // falso

As dependências de objetos e funções podem fazer com que seu Efeito seja ressincronizado com mais frequência do que o necessário.

É por isso que, sempre que possível, você deve tentar evitar objetos e funções como dependências de seu Efeito. Em vez disso, tente movê-los para fora do componente, dentro do Efeito, ou extrair valores primitivos deles.

Mova objetos e funções estáticas para fora do seu componente

Se o objeto não depender de nenhuma propriedade e estado, você poderá movê-lo para fora do seu componente:

const options = {
serverUrl: 'https://localhost:1234',
roomId: 'música'
};

function ChatRoom() {
const [message, setMessage] = useState('');

useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ Todas as dependências declaradas
// ...

Dessa forma, você prova para o linter que ele não é reativo. Como ele não pode mudar como resultado de uma re-renderização, não precisa ser uma dependência. Agora, re-renderizar ChatRoom não fará com que seu Efeito se re-sincronize.

Isso também funciona para funções:

function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: 'música'
};
}

function ChatRoom() {
const [message, setMessage] = useState('');

useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ Todas as dependências declaradas
// ...

Como o createOptions é declarado fora de seu componente, ele não é um valor reativo. É por isso que ele não precisa ser especificado nas dependências de seu Efeito e nunca fará com que seu Efeito seja ressincronizado.

Mova objetos e funções dinâmicos dentro de seu Efeito

Se o seu objeto depender de algum valor reativo que possa mudar como resultado de uma nova renderização, como um prop roomId, você não poderá movê-lo para fora do seu componente. No entanto, você pode transferir sua criação para dentro do código do seu Efeito:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Todas as dependências declaradas
// ...

Agora que options foi declarado dentro de seu Efeito, ele não é mais uma dependência de seu Efeito. Em vez disso, o único valor reativo usado por seu Efeito é roomId. Como roomId não é um objeto ou uma função, você pode ter certeza de que ele não será intencionalmente diferente. Em JavaScript, os números e as cadeias de caracteres são comparados por seu conteúdo:

// Durante a primeira renderização
const roomId1 = 'música';

// Durante a próxima renderização
const roomId2 = 'música';

// Essas duas cordas são iguais!
console.log(Object.is(roomId1, roomId2)); // Verdadeiro

Graças a essa correção, o bate-papo não será mais reconectado se você editar a entrada:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return (
    <>
      <h1>Bem-vindo(a) à sala {roomId}!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('geral');
  return (
    <>
      <label>
        Escolha a sala de bate-papo:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="geral">geral</option>
          <option value="viagem">viagem</option>
          <option value="música">música</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

No entanto, ele se reconecta quando você altera o menu suspenso roomId, conforme esperado.

Isso também funciona para funções:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

useEffect(() => {
function createOptions() {
return {
serverUrl: serverUrl,
roomId: roomId
};
}

const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Todas as dependências declaradas
// ...

Você pode escrever suas próprias funções para agrupar partes da lógica dentro do seu Efeito. Desde que você também as declare dentro do seu Efeito, elas não são valores reativos e, portanto, não precisam ser dependências do seu Efeito.

Leia valores primitivos a partir de objetos

Às vezes, você pode receber um objeto de props:

function ChatRoom({ options }) {
const [message, setMessage] = useState('');

useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ Todas as dependências declaradas
// ...

O risco aqui é que o componente pai crie o objeto durante a renderização:

<ChatRoom
roomId={roomId}
options={{
serverUrl: serverUrl,
roomId: roomId
}}
/>

Isso faria com que seu Efeito fosse reconectado toda vez que o componente pai fosse renderizado novamente. Para corrigir isso, leia as informações do objeto fora do Efeito e evite ter dependências de objetos e funções:

function ChatRoom({ options }) {
const [message, setMessage] = useState('');

const { roomId, serverUrl } = options;
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ Todas as dependências declaradas
// ...

A lógica pode se tornar um pouco repetitiva (você lê alguns valores de um objeto fora de um Efeito e, em seguida, cria um objeto com os mesmos valores dentro do Efeito). Mas isso torna muito explícito em quais informações seu Efeito realmente depende. Se um objeto for recriado inadvertidamente pelo componente pai, o chat não será reconectado. No entanto, se options.roomId ou options.serverUrl forem realmente diferentes, o chat será reconectado.

Calcular valores primitivos a partir de funções

A mesma abordagem pode funcionar para funções. Por exemplo, suponha que o componente pai passe uma função:

<ChatRoom
roomId={roomId}
getOptions={() => {
return {
serverUrl: serverUrl,
roomId: roomId
};
}}
/>

Para evitar que ele se torne uma dependência (e cause a reconexão em novas renderizações), obtenha-o fora do Efeito. Isso fornece os valores roomId e serverUrl que não são objetos e que você pode ler de dentro do seu Efeito:

function ChatRoom({ getOptions }) {
const [message, setMessage] = useState('');

const { roomId, serverUrl } = getOptions();
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ Todas as dependências declaradas
// ...

Isso só funciona para funções puras porque elas são seguras para chamar durante a renderização. Se sua função for um manipulador de eventos, mas você não quiser que suas alterações ressincronizem seu Efeito, envolva-a em um Evento de Efeito em vez disso.

Recap

  • As dependências devem sempre refletir o código atual.
  • Se você não estiver satisfeito com suas dependências, a edição deve ser feita no código.
  • Ignorar o linter pode causar bugs confusos; você deve sempre evitar essa prática.
  • Para remover uma dependência, você precisa “provar” ao linter que ela não é necessária.
  • Se algum código deve ser executado em resposta a uma interação específica, mova esse código para um manipulador de eventos.
  • Se diferentes partes do seu efeito devem ser executadas novamente por motivos distintos, divida-o em vários efeitos.
  • Se você precisar atualizar um estado com base no estado anterior, utilize uma função atualizadora.
  • Se você quiser ler o valor mais recente sem reagir a ele, extraia um Evento de Efeito do seu Efeito.
  • Em JavaScript, objetos e funções são considerados diferentes se forem criados em momentos distintos.
  • Evite depender de objetos e funções. Mova-os para fora do componente ou para dentro do Efeito.

Challenge 1 of 4:
Fixar um intervalo de reinicialização

Esse Efeito configura um intervalo que passa a cada segundo. Você notou que algo estranho está acontecendo: parece que o intervalo é destruído e recriado toda vez que ele faz tique-taque. Corrija o código para que o intervalo não seja recriado constantemente.

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('✅ Criando um intervalo');
    const id = setInterval(() => {
      console.log('⏰ Intervalo de tique-taque');
      setCount(count + 1);
    }, 1000);
    return () => {
      console.log('❌ Limpar o intervalo');
      clearInterval(id);
    };
  }, [count]);

  return <h1>Contador: {count}</h1>
}