Removendo Dependências de Efeito
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 últimas props e state do seu componente. Dependências desnecessárias podem fazer com que seu Effect execute muito frequentemente, ou até mesmo criar um loop infinito. Siga este guia para revisar e remover dependências desnecessárias de seus Effects.
Você aprenderá
- Como corrigir loops infinitos de dependência de Effect
- O que fazer quando você deseja remover uma dependência
- Como ler um valor do seu Effect sem “reagir” a ele
- Como e por que evitar dependências de objeto e função
- Por que suprimir o linter de dependência é perigoso e o que fazer em vez disso
Dependências devem corresponder ao código
Quando você escreve um Effect, você primeiro especifica como iniciar e parar o que quer que você queira que seu Effect esteja fazendo:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
// ...
}
Então, se você deixar as dependências do Effect vazias ([]
), o linter irá 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 à sala {roomId}!</h1>; } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Escolha a sala de bate-papo:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">geral</option> <option value="travel">viagem</option> <option value="music">música</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 “react” a valores reativos. Como roomId
é um valor reativo (ele pode mudar devido a um re-render), o linter verifica se você o especificou como uma dependência. Se roomId
receber um valor diferente, o React irá re-sincronizar seu Effect. Isso garante que o chat permaneça conectado à sala selecionada e “reage” 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>Bem-vindo à sala {roomId}!</h1>; } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Escolha a sala de bate-papo:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">geral</option> <option value="travel">viagem</option> <option value="music">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 Effect. Cada valor reativo usado pelo código do seu Effect deve ser declarado em 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]); // ✅ Portanto, você deve especificar esse valor reativo como uma dependência do seu Effect
// ...
}
Valores reativos incluem props e todas as variáveis e functions 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();
}, []); // 🔴 React Hook useEffect has a missing dependency: 'roomId'
// ...
}
E o linter estaria certo! Como roomId
pode mudar ao longo do tempo, isso introduziria um erro no seu código.
Para remover uma dependência, “prove” para o 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 vai mudar nos re-renders:
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 um re-render), 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>Bem-vindo à sala {roomId}!</h1>; }
É por isso que você agora pode especificar uma lista de dependências vazia ([]
). Seu Effect realmente não depende mais de nenhum valor reativo, então ele realmente não precisa ser executado novamente quando qualquer prop ou state do componente mudar.
Para alterar as dependências, altere o código
Você pode ter notado um padrão em seu fluxo de trabalho:
- Primeiro, você altera o código do seu Effect ou como seus valores reativos são declarados.
- Então, 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 a primeira etapa (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 reactivos usados pelo código do seu Effect. 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 resolver uma equação. Você pode começar com um objetivo (por exemplo, remover uma dependência) e precisa “encontrar” o código correspondente a esse objetivo. Nem todo mundo acha a resolução de equações divertida, e o mesmo pode ser dito sobre a escrita de Effects! Felizmente, há uma lista de receitas comuns que você pode experimentar abaixo.
Deep Dive
Suprimir o linter leva a erros 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> Contador: {count} <button onClick={() => setCount(0)}>Redefinir</button> </h1> <hr /> <p> A cada segundo, incrementar em: <button disabled={increment === 0} onClick={() => { setIncrement(i => i - 1); }}>–</button> <b>{increment}</b> <button onClick={() => { setIncrement(i => i + 1); }}>+</button> </p> </> ); }
Digamos que você quisesse executar o Effect “apenas no mount”. Você leu que as dependências vazias ([]
) fazem isso, então você decidiu ignorar o linter e especificou à força []
como as dependências.
Este contador deveria aumentar 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 depende de nada, o React continua a usar a function onTick
do render inicial para sempre. Durante esse render, count
era 0
e increment
era 1
. É por isso que onTick
desse render sempre chama setCount(0 + 1)
a cada segundo, e você sempre vê 1
. Erros como esse são mais difíceis de corrigir quando são espalhados por vários componentes.
Sempre existe 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 de linter de dependência como um erro de compilação. Se você não o suprimir, nunca verá erros como esse. O restante desta página documenta as alternativas para este e outros casos.
Removendo dependências desnecessárias
Toda vez que você ajusta as dependências do Effect para refletir o código, observe a lista de dependências. Faz sentido para o Effect ser executado novamente quando qualquer uma dessas dependências mudar? Às vezes, a resposta é “não”:
- Você pode querer re-executar diferentes partes do seu Effect sob diferentes condições.
- Você pode querer apenas ler o último valor de alguma dependência em vez de “reagir” às suas alterações.
- Uma dependência pode mudar com muita frequência intencionalmente porque é um objeto ou uma function.
Para encontrar a solução certa, você precisará responder a algumas perguntas sobre o seu Effect. Vamos analisá-las.
Este código deve ser movido para um manipulador de eventos?
A primeira coisa que você deve pensar é se este código deveria ser um Effect.
Imagine um formulário. No envio, você define a variável de state submitted
como true
. Você precisa enviar uma solicitação POST e exibir uma notificação. Você colocou essa lógica dentro de um Effect que “reage” ao submitted
ser true
:
function Form() {
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
// 🔴 Evite: lógica específica do evento dentro de um Effect
post('/api/register');
showNotification('Registrado com sucesso!');
}
}, [submitted]);
function handleSubmit() {
setSubmitted(true);
}
// ...
}
Mais tarde, você deseja 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) {
// 🔴 Evite: lógica específica do evento dentro de um Effect
post('/api/register');
showNotification('Registrado com sucesso!', theme);
}
}, [submitted, theme]); // ✅ Todas as dependências declaradas
function handleSubmit() {
setSubmitted(true);
}
// ...
}
Ao fazer isso, você introduziu um erro. Imagine que você envia o formulário primeiro e depois alterna entre os temas Escuro e Claro. O theme
mudará, o Effect será executado novamente e, portanto, ele exibirá a mesma notificação novamente!
O problema aqui é que isso não deveria ser um Effect 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 dos manipuladores de eventos
post('/api/register');
showNotification('Registrado com sucesso!', theme);
}
// ...
}
Agora que o código está em um manipulador de eventos, ele não é reativo - então ele só será executado quando o usuário enviar o formulário. Leia mais sobre a escolha 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 em que o usuário precisa escolher sua cidade e área. 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 busca de dados em um Effect. Você está sincronizando o state 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 a cause).
Agora, digamos que você está adicionando uma segunda caixa de seleção para as áreas da cidade, que devem 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);
}
});
// 🔴 Evite: 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 state city
, você precisou adicionar city
à lista de dependências. Isso, por sua vez, introduziu um problema: quando o usuário seleciona uma cidade diferente, o Effect será executado novamente e chamará fetchCities(country)
. Como resultado, você estará refazendo a busca desnecessariamente na lista de cidades muitas vezes.
O problema com este código é que você está sincronizando duas coisas diferentes não relacionadas:
- Você deseja sincronizar o state
cities
com a rede com base na propcountry
. - Você deseja sincronizar o state
areas
com a rede com base no statecity
.
Divida a lógica em dois Effects, cada um dos quais reage à prop que precisa 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```js
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 é re-executado apenas se o country
mudar, enquanto o segundo Effect é re-executado quando o city
muda. Você os separou por finalidade: duas coisas diferentes são sincronizadas por dois Effects separados. Dois Effects separados possuem duas listas de dependências separadas, então eles não irão disparar um ao outro intencionalmente.
O código final é maior do 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 dividi-los. Se você estiver preocupado com duplicação, pode aprimorar esse código extraindo a lógica repetitiva para um Hook personalizado.
Você está lendo algum state para calcular o state seguinte?
Este Effect atualiza a variável de state messages
com um array recém-criado sempre 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 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, toda nova mensagem fará o chat reconectar. 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
// ...
Observe como seu Effect não lê a variável messages
agora. Você só precisa passar uma função atualizadora como msgs => [...msgs, receivedMessage]
. React coloca sua função atualizadora em uma fila e fornecerá o argumento msgs
para ela durante a próxima renderização. É por isso que o próprio Effect não precisa mais depender de messages
. Como resultado dessa correção, receber uma mensagem de chat não fará mais o chat reconectar.
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 “Mute”), o Effect será ressincronizado e, por fim, se reconectará ao chat. Essa não é a experiência do usuário desejada! (Neste exemplo, mesmo desabilitar o linter não funcionaria - se você fizer isso, isMuted
ficaria “preso” com seu valor antigo.)
Para resolver esse problema, você precisa extrair a lógica que não deve ser reativa do Effect. Você não quer que este Effect “reaja” às mudanças em isMuted
. Mova este trecho de lógica não reativa para um Effect Event:
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
// ...
Effect Events 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 valores mais recentes, como onMessage
lê isMuted
). Agora que você lê isMuted
dentro de um Effect Event, ele não precisa ser uma dependência do seu Effect. Como resultado, o chat não se reconectará quando você ativar e desativar a configuração “Mute”, resolvendo o problema original!
Encapsulando um manipulador de eventos das props
Você pode se deparar com um problema semelhante quando seu componente recebe um manipulador de eventos como uma 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 faria com que o Effect fosse ressincronizado após cada re-renderização do pai. Isso faria com que ele se reconectasse ao chat. Para resolver isso, encapsule a chamada em um Effect Event:
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
// ...
Effect Events não são reativos, então você não precisa especificá-los como dependências. Como resultado, o chat não se reconectará mesmo se o componente pai passar uma função diferente a cada re-renderização.
Separando código reativo e não reativo
Neste exemplo, você deseja registrar uma visita toda vez que roomId
mudar. Você deseja incluir a notificationCount
atual com cada log, mas você não deseja que uma mudança em notificationCount
acione um log.
A solução é, novamente, separar o código não reativo em um Effect Event:
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 em relação ao roomId
, então lê roomId
dentro do seu Effect. No entanto, você não deseja que uma alteração em notificationCount
registre uma visita extra, então lê notificationCount
dentro do Effect Event. Saiba mais sobre como ler as últimas props e state dos Effects usando os Effect Events.
Algum valor reativo muda sem querer?
À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 na perspectiva do usuário. Por exemplo, digamos que você crie um objeto options
no corpo do seu componente e, em seguida, leia esse objeto 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, também há um problema com o código acima. Para vê-lo, tente digitar na 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(''); // Desabilite 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>Bem-vindo(a) à sala {roomId}!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Escolha a sala de chat:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">geral</option> <option value="travel">viagem</option> <option value="music">música</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
No sandbox acima, a entrada só atualiza a variável de state message
. Da perspectiva 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
. React vê que o objeto options
é um objeto diferente do objeto options
criado durante a última renderização. É por isso que ele re-sincroniza seu Effect (que depende de options
) e o chat se reconecta enquanto você digita.
Este problema afeta apenas objetos e funções. No JavaScript, cada objeto e função recém-criado é considerado distinto 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)); // false
As dependências de objetos e funções podem fazer com que seu Effect seja ressincronizado com mais frequência do que você precisa.
É 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, dentro do Effect ou extrair valores primitivos deles.
Mova objetos e funções estáticos para fora do seu componente
Se o objeto não depender de nenhuma prop e state, 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 para o linter que não é reativo. Ele não pode mudar como resultado de um re-render, então não precisa ser uma dependência. Agora, a renderização de ChatRoom
não fará com que seu Effect seja ressincronizado.
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, 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á com que seu Effect seja ressincronizado.
Mova objetos e funções dinâmicos para dentro do seu Effect
Se seu objeto depende de algum valor reativo que pode mudar como resultado de um re-render, 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 pelo seu conteúdo:
// Durante a primeira renderização
const roomId1 = 'music';
// Durante a próxima renderização
const roomId2 = 'music';
// Essas duas strings são as mesmas!
console.log(Object.is(roomId1, roomId2)); // true
Graç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>Bem-vindo(a) à sala {roomId}!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Escolha a sala de chat:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">geral</option> <option value="travel">viagem</option> <option value="music">música</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
Entretanto, ele se reconecta quando você muda o dropdown roomId
, como você esperaria.
Isto 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]); // ✅ All dependencies declared
// ...
Você pode escrever suas próprias funções para agrupar pedaços de lógica dentro de seu Effect. Contanto que você declare-as dentro de seu Effect, elas não são valores reativos, e então elas não precisam ser dependências de seu Effect.
Leia valores primitivos de objetos
Às vezes, você pode receber um objeto das props:
function ChatRoom({ options }) {
const [message, setMessage] = useState('');
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
// ...
O risco aqui é que o componente pai criará 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 re-renderizasse. 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]); // ✅ All dependencies declared
// ...
A lógica fica um pouco repetitiva (você lê alguns valores de um objeto fora de um Effect, e então cria um objeto com os mesmos valores dentro do Effect). Mas isso torna muito explícito em que informação seu Effect realmente depende. Se um objeto é recriado intencionalmente pelo componente pai, o chat não se reconectaria. Entretanto, se options.roomId
ou options.serverUrl
realmente forem diferentes, o chat se reconectaria.
Calcule valores primitivos 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 fazer com que ela se reconecte em re-renders), chame-a fora do Effect. Isso te dá os valores roomId
e serverUrl
que não são objetos e que você pode ler de dentro de 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]); // ✅ All dependencies declared
// ...
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 quer que suas mudanças ressinconizem seu Effect, encapsule-a em um Evento de Effect ao invés disso.
Recap
- As dependências devem sempre corresponder ao código.
- Quando você não está feliz com suas dependências, o que você precisa editar é o código.
- Suprimir o lint resulta em erros muito confusos, e você deve sempre evitá-lo.
- Para remover uma dependência, você precisa “provar” ao lint que ela não é necessária.
- Se algum código deve rodar em resposta a uma interação específica, mova esse código para um manipulador de eventos.
- Se diferentes partes de seu Effect devem rodar novamente por razões diferentes, divida-o em vários Effects.
- Se você quer atualizar algum state baseado no state anterior, passe uma função atualizadora.
- Se você quer ler o último valor sem “reagir” a ele, extraia um Evento de Effect de 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-as para fora do componente ou para dentro do Effect.
Challenge 1 of 4: Corrija um intervalo reiniciando
Este Effect configura um intervalo que tica a cada segundo. Você notou algo estranho acontecendo: parece que o intervalo é destruído e recriado toda vez que ele tica. 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('✅ Criando um intervalo'); const id = setInterval(() => { console.log('⏰ Tick do intervalo'); setCount(count + 1); }, 1000); return () => { console.log('❌ Limpando um intervalo'); clearInterval(id); }; }, [count]); return <h1>Contador: {count}</h1> }