Removing Effect Dependencies
Quando você escreve um Effect, o linter verificará se você incluiu todos os valores reativos (como props e state) que o Effect lê na lista de dependências do seu Effect. Isso garante que seu Effect permaneça sincronizado com as props e o state mais recentes do seu componente. Dependências desnecessárias podem fazer com que seu Effect seja executado com muita frequência ou até mesmo crie um loop infinito. Siga este guia para revisar e remover dependências desnecessárias dos seus Effects.
Você aprenderá
- Como corrigir loops infinitos de dependência de Effect
- O que fazer quando você quer remover uma dependência
- Como ler um valor do seu Effect 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 Effect, primeiro especifica como iniciar e parar o que quer que seu Effect esteja fazendo:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
// ...
}Em seguida, se você deixar as dependências do Effect 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>Welcome to the {roomId} room!</h1>; } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
Preencha-as de acordo com o que o linter diz:
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Todas as dependências declaradas
// ...
}Effects “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 re-sincronizará seu Effect. Isso garante que o chat permaneça conectado à sala selecionada e “reaja” ao dropdown:
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>Welcome to the {roomId} room!</h1>; } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
Para remover uma dependência, prove que ela não é uma dependência
Note que você não pode “escolher” as dependências do seu Effect. Todo valor reativo usado pelo código do seu Effect deve ser declarado na sua lista de dependências. A lista de dependências é determinada pelo código circundante:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) { // Este é um valor reativo
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Este Effect lê esse valor reativo
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Então você deve especificar esse valor reativo como uma dependência do seu Effect
// ...
}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 permitiria:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 O React Hook useEffect tem uma dependência faltando: 'roomId'
// ...
}E o linter estaria certo! Como roomId pode mudar com o tempo, isso introduziria 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 provar que ele não é reativo e não mudará em re-renderizações:
const serverUrl = 'https://localhost:1234';
const roomId = 'music'; // Não é mais 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 mudar em uma re-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 = 'music'; export default function ChatRoom() { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, []); return <h1>Welcome to the {roomId} room!</h1>; }
É por isso que você agora pode especificar uma lista de dependências vazia ([]). Seu Effect realmente não depende de nenhum valor reativo, então ele realmente não precisa ser re-executado quando qualquer uma das props ou state do componente mudar.
Para alterar as dependências, altere o código
Você pode ter notado um padrão no seu fluxo de trabalho:
- Primeiro, você altera o código do seu Effect ou como seus valores reativos são declarados.
- Em seguida, você segue o linter e ajusta as dependências para corresponder ao código que você alterou.
- Se você não estiver satisfeito com a lista de dependências, você volta para o primeiro passo (e altera o código novamente).
A última parte é importante. Se você quiser alterar as dependências, altere o código circundante primeiro. Você pode pensar na lista de dependências como uma lista de todos os valores reativos usados pelo código do seu Effect. Você não escolhe o que colocar nessa lista. A lista descreve seu código. Para alterar a lista de dependências, altere o código.
Isso pode parecer resolver uma equação. Você pode começar com um objetivo (por exemplo, remover uma dependência) e precisa “encontrar” o código que corresponde a esse objetivo. Nem todo mundo acha resolver equações divertido, e o mesmo poderia ser dito sobre escrever Effects! Felizmente, há uma lista de receitas comuns que você pode tentar abaixo.
Deep Dive
Suprimir o linter leva a bugs muito não intuitivos que são difíceis de encontrar e corrigir. Aqui está 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> Counter: {count} <button onClick={() => setCount(0)}>Reset</button> </h1> <hr /> <p> Every second, increment by: <button disabled={increment === 0} onClick={() => { setIncrement(i => i - 1); }}>–</button> <b>{increment}</b> <button onClick={() => { setIncrement(i => i + 1); }}>+</button> </p> </> ); }
Vamos dizer que você queria executar o Effect “apenas na montagem”. Você leu que dependências vazias ([]) fazem isso, então você decidiu ignorar o linter e especificou forçosamente [] como as dependências.
Este contador deveria incrementar a cada segundo pela quantidade configurável com os dois botões. No entanto, como você “mentiu” para o React que este Effect não dependia de nada, o React mantém para sempre o uso da função onTick da renderização inicial. Durante essa renderização, count era 0 e increment era 1. É por isso que onTick dessa renderização sempre chama setCount(0 + 1) a cada segundo, e você sempre vê 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 este código, você precisa adicionar onTick à lista de dependências. (Para garantir que o intervalo seja configurado apenas uma vez, faça onTick um Effect Event.)
Recomendamos tratar o erro do linter de dependência como um erro de compilação. Se você não o suprimir, nunca verá bugs como este. O restante desta página documenta as alternativas para este e outros casos.
Removendo dependências desnecessárias
Toda vez que você ajustar as dependências de um Effect para refletir o código, olhe para a lista de dependências. Faz sentido o Effect ser reexecutado quando qualquer uma dessas dependências mudar? Às vezes, a resposta é “não”:
- Você pode querer reexecutar partes diferentes do seu Effect sob condições diferentes.
- Você pode querer apenas ler o último valor de alguma dependência em vez de “reagir” às suas mudanças.
- Uma dependência pode mudar com muita frequência involuntariamente porque é um objeto ou uma função.
Para encontrar a solução correta, você precisará responder a algumas perguntas sobre seu Effect. Vamos percorrê-las.
Este código deve ser movido para um manipulador de eventos?
A primeira coisa que você deve pensar é se este código deve ser um Effect.
Imagine um formulário. Ao enviar, você define a variável de estado submitted como true. Você precisa enviar uma requisição POST e mostrar uma notificação. Você colocou essa lógica dentro de um Effect que “reage” a submitted ser true:
function Form() {
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
// 🔴 Evitar: Lógica específica de evento dentro de um Effect
post('/api/register');
showNotification('Successfully registered!');
}
}, [submitted]);
function handleSubmit() {
setSubmitted(true);
}
// ...
}Mais tarde, você quer estilizar a mensagem de notificação de acordo com o tema atual, então você lê o tema atual. Como theme é declarado no corpo do componente, é um valor reativo, então você o adiciona como uma dependência:
function Form() {
const [submitted, setSubmitted] = useState(false);
const theme = useContext(ThemeContext);
useEffect(() => {
if (submitted) {
// 🔴 Evitar: Lógica específica de evento dentro de um Effect
post('/api/register');
showNotification('Successfully registered!', theme);
}
}, [submitted, theme]); // ✅ Todas as dependências declaradas
function handleSubmit() {
setSubmitted(true);
}
// ...
}Ao fazer isso, você introduziu um bug. Imagine que você envia o formulário primeiro e depois alterna entre os temas Escuro e Claro. O theme mudará, o Effect será reexecutado e, portanto, exibirá a mesma notificação novamente!
O problema aqui é que isso não deveria ser um Effect em primeiro lugar. Você quer enviar esta requisiçã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: Lógica específica de evento é chamada a partir de manipuladores de eventos
post('/api/register');
showNotification('Successfully registered!', theme);
}
// ...
}Agora que o código está em um manipulador de eventos, ele não é reativo - portanto, ele só será executado quando o usuário enviar o formulário. Leia mais sobre escolher entre manipuladores de eventos e Effects e como excluir Effects desnecessários.
Seu Effect está fazendo várias coisas não relacionadas?
A próxima pergunta que você deve se fazer é se seu Effect está fazendo várias coisas não relacionadas.
Imagine que você está criando um formulário de envio onde o usuário precisa escolher sua cidade e região. Você busca a lista de cities do servidor de acordo com o country selecionado para mostrá-las em um dropdown:
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
// ...Este é um bom exemplo de buscar dados em um Effect. Você está sincronizando o estado cities com a rede de acordo com a prop country. Você não pode fazer isso em um manipulador de eventos porque precisa buscar assim que ShippingForm for exibido e sempre que o country mudar (não importa qual interação o cause).
Agora, digamos que você esteja adicionando uma segunda caixa de seleção para regiões da cidade, que deve buscar as areas para a city atualmente selecionada. Você pode começar adicionando uma segunda chamada fetch para a lista de áreas dentro do mesmo Effect:
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);
}
});
// 🔴 Evitar: Um único Effect 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
// ...No entanto, como o Effect 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 seleciona uma cidade diferente, o Effect será reexecutado e chamará fetchCities(country). Como resultado, você buscará desnecessariamente a lista de cidades muitas vezes.
O problema com este código é que você está sincronizando duas coisas diferentes e não relacionadas:
- Você quer sincronizar o estado
citiescom a rede com base na propcountry. - Você quer sincronizar o estado
areascom a rede com base no estadocity.
Divida a lógica em dois Effects, cada um dos quais reage à prop com a qual ele 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 Effect só é reexecutado se o country mudar, enquanto o segundo Effect é reexecutado quando o city muda. Você os separou por propósito: duas coisas diferentes são sincronizadas por dois Effects separados. Dois Effects separados têm duas listas de dependências separadas, então eles não se acionarão acidentalmente.
O código final é mais longo que o original, mas dividir esses Effects ainda está correto. Cada Effect deve representar um processo de sincronização independente. Neste exemplo, excluir um Effect não quebra a lógica do outro Effect. Isso significa que eles sincronizam coisas diferentes, e é bom separá-los. Se você estiver preocupado com duplicação, pode melhorar este código extraindo a lógica repetitiva em um Hook personalizado.
Você está lendo algum estado para calcular o próximo estado?
Este Effect atualiza a variável de estado messages com um novo array criado toda vez que uma nova mensagem chega:
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 adiciona a nova mensagem no final. No entanto, como messages é um valor reativo lido por um Effect, 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. No entanto, como este Effect agora depende de messages, isso também ressincronizará o Effect. Então, 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 Effect. Em vez disso, passe uma função atualizadora 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
// ...Note como seu Effect não lê mais a variável messages de forma alguma. 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 Effect em si não precisa mais depender de messages. Como resultado dessa correção, receber uma mensagem de chat não fará mais com que o chat se reconecte.
Você quer ler um valor sem “reagir” às suas mudanças?
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 Effect agora usa isMuted em seu código, você tem que 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 muda (por exemplo, quando o usuário pressiona o botão “Mudo”), o Effect será ressincronizado e se reconectará ao chat. Esta não é a experiência do usuário desejada! (Neste exemplo, mesmo desabilitar o linter não funcionaria - se você fizer isso, isMuted ficará “preso” com seu valor antigo.)
Para resolver este problema, você precisa extrair a lógica que não deve ser reativa para fora do Effect. Você não quer que este Effect “reaja” às mudanças em isMuted. Mova esta parte da lógica não reativa para um Evento de Effect:
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
// ...Eventos de Effect permitem que você divida um Effect em partes reativas (que devem “reagir” a valores reativos como roomId e suas mudanças) e partes não reativas (que apenas leem seus últimos valores, como onMessage lê isMuted). Agora que você lê isMuted dentro de um Evento de Effect, ele não precisa ser uma dependência do seu Effect. Como resultado, o chat não se reconectará quando você alternar a configuração “Mudo” para ligar e desligar, resolvendo o problema original!
Envolvendo um manipulador de eventos das props
Você pode encontrar um problema semelhante quando seu componente recebe um manipulador de eventos como prop:
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 onReceiveMessage é uma dependência, isso causaria a ressincronização do Effect após cada renderização do pai. Isso faria com que ele se reconectasse ao chat. Para resolver isso, envolva a chamada em um Evento de Effect:
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
// ...Eventos de Effect não são reativos, então você não precisa especificá-los como dependências. Como resultado, o chat não se reconectará mais, mesmo que o componente pai passe uma função diferente a cada renderização.
Separando código reativo e não reativo
Neste exemplo, você quer registrar uma visita toda vez que roomId mudar. Você quer incluir o notificationCount atual com cada log, mas você não quer que uma mudança em notificationCount acione um evento de log.
A solução é novamente extrair o código não reativo para um Evento de Effect:
function Chat({ roomId, notificationCount }) {
const onVisit = useEffectEvent(visitedRoomId => {
logVisit(visitedRoomId, notificationCount);
});
useEffect(() => {
onVisit(roomId);
}, [roomId]); // ✅ Todas as dependências declaradas
// ...
}Você quer que sua lógica seja reativa em relação a roomId, então você lê roomId dentro do seu Effect. No entanto, você não quer que uma mudança em notificationCount registre uma visita extra, então você lê notificationCount dentro do Evento de Effect. Saiba mais sobre como ler as últimas props e estado de Effects usando Eventos de Effect.
Algum valor reativo muda não intencionalmente?
Às vezes, você quer que seu Effect “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 Effect:
function ChatRoom({ roomId }) {
// ...
const options = {
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options);
connection.connect();
// ...Este objeto é declarado no corpo do componente, então é um valor reativo. Quando você lê um valor reativo como este dentro de um Effect, você o declara como uma dependência. Isso garante que seu Effect “reaja” às suas mudanças:
// ...
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 mudar, seu Effect se reconectará ao chat com as novas options. No entanto, há também um problema com o código acima. Para vê-lo, tente digitar na caixa de entrada no sandbox 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(''); // Desabilita temporariamente o 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>Welcome to the {roomId} room!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
No sandbox acima, a entrada apenas atualiza a variável de estado message. Do ponto de vista do usuário, isso não deve afetar a conexão do chat. No entanto, toda vez que você atualiza a message, seu componente é renderizado novamente. Quando seu componente é renderizado novamente, o código dentro dele é executado novamente do zero.
Um novo objeto options é criado do zero a cada re-renderização do componente ChatRoom. O React vê que o objeto options é um objeto diferente do objeto options criado durante a última renderização. É por isso que ele resincroniza seu Effect (que depende de options), e o chat se reconecta enquanto você digita.
Este 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 que o conteúdo dentro deles possa ser o mesmo!
// Durante a primeira renderização
const options1 = { serverUrl: 'https://localhost:1234', roomId: 'music' };
// Durante a próxima renderização
const options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };
// Estes são dois objetos diferentes!
console.log(Object.is(options1, options2)); // falseDependências de objetos e funções podem fazer com que seu Effect resincronize 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 do seu Effect. Em vez disso, tente movê-los para fora do componente, para dentro do Effect, ou extrair valores primitivos deles.
Mover objetos e funções estáticos para fora do seu componente
Se o objeto não depende de nenhuma prop ou estado, você pode mover esse objeto para fora do seu componente:
const options = {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};
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 ao linter que não é reativo. Ele não pode mudar como resultado de uma re-renderização, então não precisa ser uma dependência. Agora, re-renderizar ChatRoom não fará com que seu Effect resincronize.
Isso funciona para funções também:
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};
}
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 createOptions é declarado fora do seu componente, ele não é um valor reativo. É por isso que ele não precisa ser especificado nas dependências do seu Effect, e por que ele nunca fará seu Effect resincronizar.
Mover objetos e funções dinâmicos para dentro do seu Effect
Se o seu objeto depende de algum valor reativo que pode mudar como resultado de uma re-renderização, como uma prop roomId, você não pode puxá-lo para fora do seu componente. Você pode, no entanto, mover sua criação para dentro do código do seu Effect:
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 é declarado dentro do seu Effect, ele não é mais uma dependência do seu Effect. Em vez disso, o único valor reativo usado pelo seu Effect é roomId. Como roomId não é um objeto ou função, você pode ter certeza de que ele não será intencionalmente diferente. Em JavaScript, números e strings são comparados por seu conteúdo:
// Durante a primeira renderização
const roomId1 = 'music';
// Durante a próxima renderização
const roomId2 = 'music';
// Estas duas strings são iguais!
console.log(Object.is(roomId1, roomId2)); // trueGraças a essa correção, o chat não se reconecta mais 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>Welcome to the {roomId} room!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
No entanto, ele se reconecta quando você muda o dropdown roomId, como seria de se esperar.
Isso funciona para funções também:
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 Effect. Desde que você também as declare dentro do seu Effect, elas não são valores reativos, e portanto não precisam ser dependências do seu Effect.
Ler valores primitivos 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 Effect se reconectasse toda vez que o componente pai fosse renderizado novamente. Para corrigir isso, leia informações do objeto fora do Effect 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 fica um pouco repetitiva (você lê alguns valores de um objeto fora de um Effect e, em seguida, cria um objeto com os mesmos valores dentro do Effect). Mas torna explícito quais informações seu Effect realmente depende. Se um objeto for recriado não intencionalmente pelo componente pai, o chat não se reconectará. No entanto, se options.roomId ou options.serverUrl forem realmente diferentes, o chat se reconectará.
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 torná-la uma dependência (e causar sua reconexão em re-renderizações), chame-a fora do Effect. Isso lhe dá os valores roomId e serverUrl que não são objetos, e que você pode ler de dentro do seu Effect:
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 serem chamadas durante a renderização. Se sua função for um manipulador de eventos, mas você não quiser que suas mudanças resincronizem seu Effect, envolva-a em um Effect Event em vez disso.
Recap
- As dependências devem sempre corresponder ao código.
- Quando você não está satisfeito com suas dependências, o que você precisa editar é o código.
- Suprimir o linter leva a bugs muito confusos, e você deve sempre evitá-lo.
- 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 partes diferentes do seu Effect devem ser executadas novamente por diferentes motivos, divida-o em vários Effects.
- Se você deseja atualizar algum estado com base no estado anterior, passe uma função atualizadora.
- Se você deseja ler o valor mais recente sem “reagir” a ele, extraia um Effect Event do seu Effect.
- Em JavaScript, objetos e funções são considerados diferentes se foram criados em momentos diferentes.
- Tente evitar dependências de objetos e funções. Mova-os para fora do componente ou para dentro do Effect.
Challenge 1 of 4: Corrigir um intervalo que reinicia
Este Effect configura um intervalo que conta a cada segundo. Você notou algo estranho acontecendo: parece que o intervalo está sendo destruído e recriado a cada tick. Corrija o código para que o intervalo não seja constantemente recriado.
import { useState, useEffect } from 'react'; export default function Timer() { const [count, setCount] = useState(0); useEffect(() => { console.log('✅ Creating an interval'); const id = setInterval(() => { console.log('⏰ Interval tick'); setCount(count + 1); }, 1000); return () => { console.log('❌ Clearing an interval'); clearInterval(id); }; }, [count]); return <h1>Counter: {count}</h1> }