Устранение зависимостей эффектов
При написании эффекта интерпретатор проверит, что вы включили все реактивные значения (например, props и состояние), которые использует эффект, в список зависимостей эффекта. Это гарантирует, что эффект остается синхронизированным с последними значениями props и состояния компонента. Излишние зависимости могут привести к тому, что эффект будет выполняться слишком часто или даже к созданию бесконечного цикла. Следуйте этому руководству, чтобы проверить и удалить нежелательные зависимости из своих эффектов.
Вы узнаете
- Как исправить бесконечные циклы зависимостей эффектов
- Что делать, если нужно удалить зависимость
- Как читать значение из эффекта без «реагирования» на него
- Как и почему избегать зависимостей от объектов и функций
- Почему подавление проверки зависимостей опасно и что делать вместо этого
Зависимости должны соответствовать коду
При написании эффекта вы сначала определяете, как начать и остановить то, что должен делать ваш эффект:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
// ...
} Затем, если вы оставите список зависимостей эффекта пустым ([]), интерпретатор предложит правильные зависимости:
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(); }, []); // <-- Fix the mistake here! 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} /> </> ); }
Заполните их в соответствии с подсказками интерпретатора:
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
} Эффекты «реагируют» на реактивные значения. Поскольку roomId является реактивным значением (оно может измениться из-за повторной отрисовки), интерпретатор проверяет, что вы указали его в качестве зависимости. Если roomId получит другое значение, React пересинхронизирует ваш эффект. Это гарантирует, что чат остается подключенным к выбранному помещению и «реагирует» на выпадающий список:
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} /> </> ); }
Чтобы удалить зависимость, докажите, что она не является зависимостью
Обратите внимание, что вы не можете «выбирать» зависимости вашего эффекта. Каждое реактивное значение, используемое кодом вашего эффекта, должно быть объявлено в вашем списке зависимостей. Список зависимостей определяется окружающим кодом:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) { // This is a reactive value
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads that reactive value
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ So you must specify that reactive value as a dependency of your Effect
// ...
} Реактивные значения включают props и все переменные и функции, объявленные непосредственно внутри вашего компонента. Поскольку roomId является реактивным значением, вы не можете удалить его из списка зависимостей. Интерпретатор этого не позволит:
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'
// ...
} И интерпретатор будет прав! Поскольку roomId может меняться со временем, это введет ошибку в ваш код.
Чтобы удалить зависимость, «докажите» интерпретатору, что она не нуждается в качестве зависимости. Например, вы можете перенести roomId из вашего компонента, чтобы доказать, что она не реактивна и не будет изменяться при повторных отрисовках:
const serverUrl = 'https://localhost:1234';
const roomId = 'music'; // Not a reactive value anymore
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...
} Теперь, поскольку roomId не является реактивным значением (и не может изменяться при повторной отрисовке), она не нуждается в качестве зависимости:
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>; }
Вот почему вы теперь можете указать пустой ([]) список зависимостей. Ваш эффект действительно не зависит ни от какого реактивного значения больше, поэтому он действительно не должен перевыполняться, когда меняются какие-либо props или состояние компонента.
Чтобы изменить зависимости, измените код
Вы, возможно, заметили закономерность в своем рабочем процессе:
- Во-первых, вы изменяете код своего эффекта или объявления ваших реактивных значений.
- Затем вы следуете инструкциям интерпретатора и корректируете зависимости, чтобы они соответствовали измененному коду.
- Если вас не устраивает список зависимостей, вы возвращаетесь к первому шагу (и изменяете код снова).
Последний пункт важен. Если вы хотите изменить зависимости, сначала измените окружающий код. Вы можете рассматривать список зависимостей как список всех реактивных значений, используемых кодом вашего эффекта. Вы не выбираете, что помещать в этот список. Список описывает ваш код. Чтобы изменить список зависимостей, измените код.
Это может показаться решением уравнения. Вы можете начать с цели (например, удалить зависимость) и вам нужно «найти» соответствующий код. Не все считают решение уравнений увлекательным, и то же самое можно сказать о написании эффектов! К счастью, ниже приведен список общих рецептов, которые вы можете попробовать.
Возможная ошибка
Если у вас есть существующая база кода, у вас могут быть некоторые эффекты, которые подавляют интерпретатор, например:
useEffect(() => {
// ...
// 🔴 Avoid suppressing the linter like this:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);
Когда зависимости не соответствуют коду, очень высок риск введения ошибок. Подавляя интерпретатор, вы «лжете» React о значениях, от которых зависит ваш эффект.
Вместо этого используйте описанные ниже методы.
Подробный анализ
Почему подавление проверки зависимостей так опасно?
Подавление проверки зависимостей приводит к очень неинтуитивным ошибкам, которые трудно найти и исправить. Вот один пример:
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> </> ); }
Предположим, что вы хотите запустить эффект «только при монтировании». Вы прочитали, что пустые ([]) зависимости делают это, поэтому вы решили проигнорировать интерпретатор и насильно указали [] в качестве зависимостей.
Этот счетчик должен был увеличиваться каждую секунду на величину, настраиваемую двумя кнопками. Однако, так как вы «соврали» React, что этот эффект ни от чего не зависит, React навсегда использует функцию onTick из исходной отрисовки. Во время этой отрисовки count было 0 и increment было 1. Именно поэтому onTick из этой отрисовки всегда вызывает setCount(0 + 1) каждую секунду, и вы всегда видите 1. Такие ошибки сложнее исправить, когда они распределены по нескольким компонентам.
Всегда есть лучшее решение, чем игнорирование интерпретатора! Для исправления этого кода вам нужно добавить onTick в список зависимостей. (Чтобы гарантировать, что интервал настраивается только один раз, сделайте onTick событием эффекта.)
Мы рекомендуем рассматривать ошибку проверки зависимостей как ошибку компиляции. Если вы не будете подавлять её, вы никогда не увидите такие ошибки. Остальная часть этой страницы документирует альтернативы для этого и других случаев.
Удаление ненужных зависимостей
Каждый раз, когда вы корректируете зависимости эффекта для соответствия коду, смотрите на список зависимостей. Имеет ли смысл повторное выполнение эффекта при изменении любой из этих зависимостей? Иногда ответ «нет»:
- Вы можете захотеть повторно выполнить разные части своего эффекта при разных условиях.
- Вы можете захотеть прочитать только последнее значение какой-либо зависимости, вместо того, чтобы «реагировать» на его изменения.
- Зависимость может изменяться слишком часто неожиданно, потому что это объект или функция.
Чтобы найти правильное решение, вам нужно ответить на несколько вопросов о вашем эффекте. Давайте пройдемся по ним.
Должен ли этот код переместиться в обработчик событий?
Первое, о чем следует задуматься, — это то, должен ли этот код быть эффектом вообще.
Представьте форму. При отправке вы устанавливаете переменную состояния submitted в true. Вам нужно отправить POST-запрос и отобразить уведомление. Вы поместили эту логику внутрь эффекта, который «реагирует» на то, что submitted является true. Вот код:
function Form() {
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!');
}
}, [submitted]);
function handleSubmit() {
setSubmitted(true);
}
// ...
} Позже вы хотите стилизовать сообщение уведомления в соответствии с текущей темой, поэтому вы читаете текущую тему. Поскольку theme объявлена в теле компонента, она является реактивным значением, поэтому вы добавляете её в качестве зависимости:
function Form() {
const [submitted, setSubmitted] = useState(false);
const theme = useContext(ThemeContext);
useEffect(() => {
if (submitted) {
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!', theme);
}
}, [submitted, theme]); // ✅ All dependencies declared
function handleSubmit() {
setSubmitted(true);
}
// ...
} Таким образом, вы ввели ошибку. Представьте, что вы сначала отправляете форму, а затем переключаетесь между темными и светлыми темами. theme изменится, эффект перевыполнится, и он снова отобразит то же самое уведомление!
Проблема здесь в том, что это не должно быть эффектом в первую очередь. Вы хотите отправить этот POST-запрос и отобразить уведомление в ответ на отправку формы, что является определенным взаимодействием. Чтобы выполнить какой-либо код в ответ на определённое взаимодействие, поместите эту логику непосредственно в соответствующий обработчик событий:
function Form() {
const theme = useContext(ThemeContext);
function handleSubmit() {
// ✅ Good: Event-specific logic is called from event handlers
post('/api/register');
showNotification('Successfully registered!', theme);
}
// ...
} Теперь, когда код находится в обработчике событий, он не является реактивным — поэтому он будет выполняться только при отправке пользователем формы. Подробнее об выборе между обработчиками событий и эффектами и удалении ненужных эффектов.
Выполняет ли ваш эффект несколько не связанных вещей?
Следующий вопрос, который вы должны задать себе, — это выполняет ли ваш эффект несколько не связанных действий.
Представьте, что вы создаете форму доставки, где пользователь должен выбрать город и область. Вы получаете список cities с сервера в соответствии с выбранным country для отображения их в выпадающем списке:
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]); // ✅ All dependencies declared
// ... Это хороший пример получения данных в эффекте. Вы синхронизируете состояние cities с сетью в соответствии с свойством country. Вы не можете сделать это в обработчике событий, потому что вам нужно получать данные, как только ShippingForm отображается и всякий раз, когда country изменяется (независимо от того, какое взаимодействие вызвало это).
Теперь предположим, что вы добавляете второй выпадающий список для областей города, которые должны получать данные areas для текущего выбранного city. Вы можете начать, добавив второй вызов fetch для списка областей внутри того же эффекта:
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);
}
});
// 🔴 Avoid: A single Effect synchronizes two independent processes
if (city) {
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
}
return () => {
ignore = true;
};
}, [country, city]); // ✅ All dependencies declared
// ... Однако, поскольку эффект теперь использует переменную состояния city, вам пришлось добавить city в список зависимостей. Это, в свою очередь, создало проблему: когда пользователь выбирает другой город, эффект перевыполнится и вызовет fetchCities(country). В результате вы будете излишне получать список городов много раз.
Проблема с этим кодом заключается в том, что вы синхронизируете две разные не связанные вещи:
- Вы хотите синхронизировать состояние
citiesс сетью на основе свойстваcountry. - Вы хотите синхронизировать состояние
areasс сетью на основе состоянияcity.
Разделите логику на два эффекта, каждый из которых реагирует на проп, с которым ему нужно синхронизироваться:
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]); // ✅ All dependencies declared
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]); // ✅ All dependencies declared
// ... Теперь первый эффект выполняется повторно только в том случае, если country изменяется, а второй эффект выполняется повторно, когда изменяется city. Вы разделили их по назначению: двумя разными эффектами синхронизируются две разные вещи. У двух отдельных эффектов есть два отдельных списка зависимостей, поэтому они не будут случайно срабатывать друг за другом.
Конечный код длиннее исходного, но разделение этих эффектов всё ещё правильно. Каждый эффект должен представлять собой независимый процесс синхронизации. В этом примере удаление одного эффекта не нарушает логику другого эффекта. Это означает, что они синхронизируют разные вещи, и лучше их разделить. Если вы обеспокоены дублированием, вы можете улучшить этот код, вынеся повторяющуюся логику в пользовательский хук.
Читаете ли вы какое-то состояние для вычисления следующего состояния?
Этот эффект обновляет переменную состояния messages новым массивом каждый раз, когда приходит новое сообщение:
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
// ... Он использует переменную messages, чтобы создать новый массив, начиная со всех существующих сообщений, и добавляет новое сообщение в конец. Однако, поскольку messages — это реактивное значение, считываемое эффектом, оно должно быть зависимостью:
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]); // ✅ All dependencies declared
// ... И добавление messages в качестве зависимости создаёт проблему.
Каждый раз, когда вы получаете сообщение, setMessages() заставляет компонент перерисовываться с новым массивом messages, который включает полученное сообщение. Однако, поскольку этот эффект теперь зависит от messages, это также синхронизирует эффект. Так что каждое новое сообщение будет заставлять чат повторно подключаться. Пользователь этого не захочет!
Чтобы исправить проблему, не читайте messages внутри эффекта. Вместо этого передайте функцию обновления 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]); // ✅ All dependencies declared
// ... Обратите внимание, как ваш эффект больше не считывает переменную messages вообще. Вам нужно только передать функцию обновления, такую как msgs => [...msgs, receivedMessage]. React помещает вашу функцию обновления в очередь, и во время следующего рендеринга предоставит аргумент msgs ей. Вот почему сам эффект больше не нуждается в зависимости от messages. В результате получения сообщения в чат больше не приведет к его повторному подключению.
Хотите ли вы прочитать значение, не «реагируя» на его изменения?
В разработке
Этот раздел описывает экспериментальный API, который ещё не выпущен в стабильной версии React.
Предположим, что вы хотите воспроизвести звук, когда пользователь получает новое сообщение, если isMuted не 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();
}
});
// ... Поскольку ваш эффект теперь использует isMuted в своём коде, вам нужно добавить его в зависимости:
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]); // ✅ All dependencies declared
// ... Проблема в том, что каждый раз, когда isMuted изменяется (например, когда пользователь нажимает переключатель «Без звука»), эффект будет повторно синхронизироваться и подключаться к чату. Этого пользовательского опыта не нужно! (В этом примере даже отключение линтера не сработает — если вы это сделаете, isMuted застрянет со своим старым значением.)
Для решения этой проблемы необходимо вынести из эффекта логику, которая не должна быть реактивной. Вам не нужно, чтобы этот эффект «реагировал» на изменения в isMuted. Переместите этот нереактивный фрагмент логики в событие эффекта:
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]); // ✅ All dependencies declared
// ... События эффектов позволяют разделить эффект на реактивные части (которые должны «реагировать» на реактивные значения, такие как roomId, и их изменения) и нереактивные части (которые считывают только последние значения, как onMessage считывает isMuted). Теперь, когда вы считываете isMuted внутри события эффекта, он не должен быть зависимостью вашего эффекта. В результате чат больше не будет повторно подключаться при включении и отключении настройки «Без звука», что решает исходную проблему!
Оборачивание обработчика событий из пропов
Вы можете столкнуться с похожей проблемой, когда ваш компонент получает обработчик событий как проп:
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]); // ✅ All dependencies declared
// ... Предположим, что родительский компонент передает другую функцию onReceiveMessage на каждом рендеринге:
<ChatRoom
roomId={roomId}
onReceiveMessage={receivedMessage => {
// ...
}}
/> Поскольку onReceiveMessage является зависимостью, это заставит эффект повторно синхронизироваться после каждого перерендеринга родительского компонента. Это заставит его повторно подключаться к чату. Для решения этой проблемы оберните вызов в событие эффекта:
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]); // ✅ All dependencies declared
// ... События эффектов не являются реактивными, поэтому вам не нужно указывать их в качестве зависимостей. В результате чат больше не будет повторно подключаться, даже если родительский компонент передает функцию, которая отличается при каждом перерендеринге.
Разделение реактивного и нереактивного кода
В этом примере вы хотите записать посещение каждый раз, когда roomId изменяется. Вы хотите включить текущее значение notificationCount в каждый лог, но вы не хотите, чтобы изменение notificationCount вызывало событие лога.
Решение снова состоит в том, чтобы разделить нереактивный код на событие эффекта:
function Chat({ roomId, notificationCount }) {
const onVisit = useEffectEvent(visitedRoomId => {
logVisit(visitedRoomId, notificationCount);
});
useEffect(() => {
onVisit(roomId);
}, [roomId]); // ✅ All dependencies declared
// ...
} Вы хотите, чтобы ваша логика была реактивной относительно roomId, поэтому вы читаете roomId внутри своего эффекта. Однако вы не хотите, чтобы изменение notificationCount регистрировало дополнительное посещение, поэтому вы читаете notificationCount внутри события эффекта. Узнайте больше о чтении последних пропов и состояния от эффектов с помощью событий эффектов.
Изменяется ли какое-то реактивное значение непреднамеренно?
Иногда вы хотите, чтобы ваш эффект «реагировал» на определённое значение, но это значение меняется чаще, чем вам хотелось бы, и может не отражать реального изменения со стороны пользователя. Например, давайте скажем, что вы создаёте объект options в теле вашего компонента, а затем считываете этот объект внутри своего эффекта:
function ChatRoom({ roomId }) {
// ...
const options = {
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options);
connection.connect();
// ... Этот объект объявлен в теле компонента, поэтому это реактивное значение. Когда вы считываете реактивное значение подобного рода внутри эффекта, вы объявляете его в качестве зависимости. Это гарантирует, что ваш эффект «реагирует» на его изменения:
// ...
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
// ... Важно объявить его зависимостью! Это гарантирует, например, что если roomId изменится, ваш эффект повторно подключится к чату с новым значением options. Однако в коде выше есть и проблема. Чтобы увидеть её, попробуйте ввести текст в поле ввода в песочнице ниже и посмотрите, что происходит в консоли:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); // Temporarily disable the linter to demonstrate the problem // 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} /> </> ); }
В песочнице выше поле ввода обновляет только переменную состояния message. Со стороны пользователя это не должно влиять на подключение к чату. Однако каждый раз, когда вы обновляете message, ваш компонент перерисовывается. Когда ваш компонент перерисовывается, код внутри него выполняется заново с нуля.
Новый объект options создаётся заново при каждом перерендеринге компонента ChatRoom. React видит, что объект options — это другой объект по сравнению с объектом options, созданным во время последнего рендеринга. Вот почему он повторно синхронизирует ваш эффект (который зависит от options), и чат повторно подключается по мере ввода.
Эта проблема касается только объектов и функций. В JavaScript каждый вновь созданный объект и функция считаются отличными от всех остальных. Не важно, что содержимое внутри них может быть одинаковым!
// During the first render
const options1 = { serverUrl: 'https://localhost:1234', roomId: 'music' };
// During the next render
const options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };
// These are two different objects!
console.log(Object.is(options1, options2)); // false Зависимости от объектов и функций могут заставлять ваш эффект повторно синхронизироваться чаще, чем нужно.
Вот почему, по возможности, следует избегать объектов и функций в качестве зависимостей вашего эффекта. Вместо этого попробуйте вынести их за пределы компонента, внутри эффекта или извлечь примитивные значения из них.
Вынести статические объекты и функции за пределы компонента
Если объект не зависит от каких-либо пропов и состояния, вы можете вынести этот объект за пределы компонента:
const options = {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};
function ChatRoom() {
const [message, setMessage] = useState('');
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ... Таким образом, вы доказываете линтеру, что он не реактивен. Он не может измениться в результате перерендеринга, поэтому он не нуждается в качестве зависимости. Теперь перерендеринг ChatRoom не вызовет повторной синхронизации вашего эффекта.
Это работает и для функций:
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();
}, []); // ✅ All dependencies declared
// ... Поскольку createOptions объявлен за пределами вашего компонента, он не является реактивным значением. Вот почему он не должен быть указан в зависимостях вашего эффекта и никогда не вызовет его повторной синхронизации.
Вынести динамические объекты и функции внутрь эффекта
Если ваш объект зависит от какого-то реактивного значения, которое может измениться в результате перерендеринга, например, проп roomId, вы не можете вынести его за пределы компонента. Вы можете, однако, перенести его создание внутрь кода вашего эффекта:
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]); // ✅ All dependencies declared
// ... Теперь, когда options объявлен внутри вашего эффекта, он больше не является зависимостью вашего эффекта. Вместо этого единственным реактивным значением, используемым вашим эффектом, является roomId. Поскольку roomId не является объектом или функцией, вы можете быть уверены, что он не будет неожиданно отличаться. В JavaScript числа и строки сравниваются по своему содержанию:
// During the first render
const roomId1 = 'music';
// During the next render
const roomId2 = 'music';
// These two strings are the same!
console.log(Object.is(roomId1, roomId2)); // true Благодаря этому исправлению чат больше не переподключается при редактировании поля ввода:
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} /> </> ); }
Однако он переподключается при изменении выпадающего списка roomId, как и ожидалось.
Это также работает для функций:
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
// ... Вы можете писать свои собственные функции, чтобы группировать фрагменты логики внутри вашего эффекта. До тех пор, пока вы также объявляете их внутри вашего эффекта, они не являются реактивными значениями, и поэтому им не нужно быть зависимостями вашего эффекта.
Чтение примитивных значений из объектов
Иногда вы можете получить объект из пропов:
function ChatRoom({ options }) {
const [message, setMessage] = useState('');
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
// ... Риск в том, что родительский компонент создаст объект во время рендеринга:
<ChatRoom
roomId={roomId}
options={{
serverUrl: serverUrl,
roomId: roomId
}}
/> Это заставит ваш эффект переподключаться каждый раз, когда родительский компонент перерисовывается. Чтобы исправить это, считывайте информацию из объекта вне эффекта и избегайте зависимостей от объектов и функций:
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
// ... Логика становится немного повторяющейся (вы считываете некоторые значения из объекта вне Effect, а затем создаёте объект с теми же значениями внутри Effect). Но это делает очень явным, от чего фактически зависит ваш Effect. Если объект случайно пересоздаётся родительским компонентом, чат не подключится повторно. Однако, если options.roomId или options.serverUrl действительно отличаются, чат подключится повторно.
Вычисление примитивных значений из функций
Тот же подход может работать для функций. Например, предположим, что родительский компонент передаёт функцию:
<ChatRoom
roomId={roomId}
getOptions={() => {
return {
serverUrl: serverUrl,
roomId: roomId
};
}}
/> Чтобы избежать превращения её в зависимость (и вызывая повторное подключение при повторном рендеринге), вызовите её вне Effect. Это даёт вам значения roomId и serverUrl, которые не являются объектами, и которые вы можете прочитать внутри вашего 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
// ... Это работает только для чистых функций, потому что их безопасно вызывать во время рендеринга. Если ваша функция является обработчиком событий, но вы не хотите, чтобы её изменения синхронизировали ваш Effect, оборачивайте её в Effect Event.
Резюме
- Зависимости всегда должны соответствовать коду.
- Когда вы недовольны зависимостями, вам нужно изменить код.
- Отключение линтера приводит к очень запутанным ошибкам, и вы всегда должны избегать этого.
- Чтобы удалить зависимость, вам нужно «доказать» линтеру, что она не нужна.
- Если какой-то код должен выполняться в ответ на определённое взаимодействие, переместите этот код в обработчик событий.
- Если разные части вашего Effect должны повторно запускаться по разным причинам, разделите его на несколько Effect.
- Если вы хотите обновить состояние на основе предыдущего состояния, передайте функцию обновления.
- Если вы хотите прочитать последнее значение, не «реагируя» на него, извлеките Effect Event из вашего Effect.
- В JavaScript объекты и функции считаются различными, если они были созданы в разное время.
- Старайтесь избегать зависимостей от объектов и функций. Перемещайте их за пределы компонента или внутрь Effect.
Попробуйте решить некоторые задачи
Задача 1 из 4:
Исправить сбрасывающийся интервал
Этот Effect устанавливает интервал, который тикает каждую секунду. Вы заметили странное поведение: кажется, что интервал уничтожается и пересоздаётся каждый раз, когда он тикает. Исправьте код, чтобы интервал не пересоздавался постоянно.
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> }
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://18.react.dev/learn/removing-effect-dependencies