Жизненный цикл реактивных эффектов
Эффекты имеют жизненный цикл, отличный от компонентов. Компоненты могут монтироваться, обновляться или демонтироваться. Эффект может делать только две вещи- начать синхронизацию чего-либо и позже остановить эту синхронизацию. Этот цикл может повторяться несколько раз, если ваш эффект зависит от пропсов и состояния, которые меняются со временем. React предоставляет правило линтера для проверки того, что вы правильно указали зависимости вашего эффекта зависимости эффекта. Это позволяет поддерживать синхронизацию эффекта с последними значениями props и state.
Вы узнаете
- Чем жизненный цикл эффекта отличается от жизненного цикла компонента
- Как рассматривать каждый отдельный эффект изолированно
- Когда вашему эффекту требуется повторная синхронизация и почему
- Как определяются зависимости вашего эффекта
- Что значит, когда значение является реактивным
- Что означает пустой массив зависимостей
- Как React проверяет правильность ваших зависимостей с помощью линтера
- Что делать, если вы не согласны с линтером
Жизненный цикл эффекта
Каждый компонент React проходит один и тот же жизненный цикл:
- Компонент mounts, когда он добавляется на экран.
- Компонент updates, когда он получает новые props или state, обычно в ответ на взаимодействие.
- Компонент unmounts, когда он удаляется с экрана.
Это хороший способ думать о компонентах, но не об эффектах. Вместо этого постарайтесь думать о каждом эффекте независимо от жизненного цикла вашего компонента. Эффект описывает, как [синхронизировать внешнюю систему] (/learn/synchronizing-with-effects) с текущими props и state. По мере изменения вашего кода синхронизация будет происходить чаще или реже.
Чтобы проиллюстрировать этот момент, рассмотрим этот эффект, подключающий ваш компонент к серверу чата:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}Тело вашего эффекта определяет, как начать синхронизацию:
// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
// ...Функция очистки, возвращаемая вашим эффектом, определяет, как остановить синхронизацию:
// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
// ...Интуитивно можно подумать, что React начнет синхронизацию при монтировании вашего компонента и прекратит синхронизацию при его демонтировании. Однако это еще не все! Иногда может потребоваться *запускать и останавливать синхронизацию несколько раз * пока компонент остается смонтированным.
Давайте посмотрим, почему это необходимо, когда это происходит и как вы можете контролировать это поведение.
Некоторые эффекты вообще не возвращают функцию очистки. Чаще всего вам захочется вернуть такую функцию — но если вы этого не сделаете, React будет вести себя так, как будто вы вернули пустую функцию очистки.
Почему синхронизация может потребоваться более одного раза
Представьте, что этот компонент ChatRoom получает проп roomId, который пользователь выбирает в выпадающем списке. Допустим, что изначально пользователь выбирает комнату «general» в качестве roomId. Ваше приложение отображает чат-комнату «general»:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId /* «general» */ }) {
// ...
return <h1>Добро пожаловать в комнату {roomId}!</h1>;
}После отображения пользовательского интерфейса React запустит ваш эффект, чтобы начать синхронизацию. Он подключается к комнате «general»:
function ChatRoom({ roomId /* «general» */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Подключается к комнату «general»
connection.connect();
return () => {
connection.disconnect(); // Отключается от комнаты «general»
};
}, [roomId]);
// ...Пока все хорошо.
Позже пользователь выбирает другую комнату в раскрывающемся списке (например, «travel»). Сначала React обновит интерфейс:
function ChatRoom({ roomId /* «travel» */ }) {
// ...
return <h1>Добро пожаловать в комнату {roomId}!</h1>;
}Подумайте, что должно произойти дальше. Пользователь видит, что в интерфейсе выбран чат-комната «travel». Однако эффект, запущенный в последний раз, по-прежнему подключен к комнате 'general'. Проп roomId изменился, поэтому то, что ваш эффект делал тогда (подключался к комнате «general»), больше не соответствует интерфейсу.
На данный момент вы хотите, чтобы React сделал две вещи:
- Прекратить синхронизацию со старым
roomId(отключиться от комнаты«general») - Начать синхронизацию с новым
roomId(подключиться к комнате«travel»)
К счастью, вы уже научили React, как делать обе эти вещи! Тело вашего эффекта определяет, как начать синхронизацию, а ваша функция очистки — как прекратить синхронизацию. Все, что React теперь нужно сделать, — это вызвать их в правильном порядке и с правильными пропсами и состоянием. Давайте посмотрим, как именно это происходит.
Как React повторно синхронизирует ваш эффект
Напомним, что ваш компонент ChatRoom получил новое значение для своего пропса roomId. Раньше оно было «general», а теперь — «travel». React должен повторно синхронизировать ваш эффект, чтобы переподключить вас к другой комнате.
Чтобы прекратить синхронизацию, React вызовет функцию очистки, которую ваш Effect вернул после подключения к комнате «general». Поскольку roomId было «general», функция очистки отключается от комнаты 'general':
function ChatRoom({ roomId /* «general» */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Подключается к комнате «general»
connection.connect();
return () => {
connection.disconnect(); // Отключается от комнаты «general»
};
// ...Затем React запустит эффект, который вы указали, во время этого рендеринга. На этот раз roomId равно «travel», поэтому начнется синхронизация с чатом «travel» (до тех пор, пока в конечном итоге не будет вызвана и его функция очистки):
function ChatRoom({ roomId /* «travel» */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Подключается к комнате «travel»
connection.connect();
// ...Благодаря этому вы теперь подключены к той же комнате, которую пользователь выбрал в интерфейсе. Кризис предотвращен!
Каждый раз, когда ваш компонент перерисовывается с другим roomId, ваш эффект будет синхронизироваться заново. Например, предположим, что пользователь меняет roomId с «travel» на «music». React снова прекратит синхронизацию вашего эффекта, вызвав его функцию очистки (отсоединив вас от комнаты 'travel'). Затем он снова начнет синхронизацию, запустив его тело с новым пропом roomId (подключив вас к комнате "music «).
Наконец, когда пользователь переходит на другой экран, ChatRoom отключается. Теперь нет никакой необходимости оставаться подключенным. React прекратит синхронизацию вашего эффекта в последний раз и отключит вас от чата »music".
Рассуждения с точки зрения эффекта
Давайте подведем итоги всего, что произошло, с точки зрения компонента ChatRoom:
ChatRoomсмонтирован сroomId, установленным в«general»ChatRoomобновлен сroomId, установленным в'travel'ChatRoomобновлен сroomId, установленным в«music»ChatRoomотмонтирован
На каждом из этих этапов жизненного цикла компонента ваш Effect выполнял разные действия:
- Ваш Effect подключился к комнате
" general«комнату - Ваш эффект отсоединился от комнаты
»general«и подключился к комнате»travel« - Ваш эффект отсоединился от комнаты
»travel«и подключился к комнате'music' - Ваш эффект отсоединился от комнаты
»music"
Теперь давайте посмотрим на происходящее с точки зрения самого эффекта:
useEffect(() => {
// Ваш эффект подключился к комнате, указанной с помощью roomId...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
// ...пока не отключился
connection.disconnect();
};
} , [roomId]);Структура этого кода может подсказать вам, что произошедшее следует рассматривать как последовательность непересекающихся временных отрезков:
- Ваш эффект подключился к комнате
«general»(до тех пор, пока не отключился) - Ваш эффект подключился к комнате
'travel'(до тех пор, пока не отключился) - Ваш эффект подключился к комнате
«music»(до тех пор, пока не отключился)
Раньше вы думали с точки зрения компонента. Когда вы смотрели с точки зрения компонента, было соблазнительно думать об эффектах как о «обратных вызовах» или «событиях жизненного цикла », которые срабатывают в определенный момент, например «после рендеринга» или «перед отключением». Такой подход очень быстро усложняется, поэтому его лучше избегать.
**Вместо этого всегда сосредоточивайтесь на одном цикле запуска/остановки за раз. Не должно иметь значения, монтируется ли компонент, обновляется или отключается. Все, что вам нужно сделать, — это описать, как запустить синхронизацию и как ее остановить. Если вы сделаете это правильно, ваш эффект будет устойчивым к запуску и остановке столько раз, сколько потребуется. **
Это может напомнить вам о том, что вы не задумываетесь о том, монтируется ли компонент или обновляется, когда пишете логику рендеринга, создающую JSX. Вы описываете, что должно быть на экране, а React само выясняет остальное.
Как React проверяет, что ваш Effect может повторно синхронизироваться
Вот рабочий пример, с которым вы можете поработать. Нажмите «Open chat», чтобы смонтировать компонент ChatRoom:
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>Добро пожаловать в комнату {roomId}!</h1>;
}
export default function App() {
const [roomId , setRoomId] = useState('general');
const [show, setShow] = useState(false);
return (
<>
<label>
Выберите чат-комнату:{' '}
<select
value={roomId}
onChange={e => setRoomId(e.target.value)}
>
<option value="general">общий</option>
<option value="travel">путешествия</option>
<option value="music">музыка</option>
</select>
</label>
<button onClick={() => setShow(!show)}>
{show ? 'Закрыть чат' : 'Открыть чат'}
</button>
{show && <hr />}
{show && <ChatRoom roomId={roomId} />}
</>
);
}export function createConnection(serverUrl, roomId) {
// В реальной реализации бы произошло подключение к серверу
return {
connect() {
console.log('✅ Подключение к комнате «' + roomId + '» по адресу ' + serverUrl + '...');
},
disconnect() {
console.log('❌ Отключено от комнаты «' + roomId + '» на ' + serverUrl);
}
};
}input { display: block; margin-bottom: 20px; }
button { margin-left: 10px; }Обратите внимание, что при первом запуске компонента вы увидите три записи в журнале:
✅ Подключение к комнате «general» по адресу https://localhost:1234...(только для разработки)❌ Отключено от комнаты «general» по адресу https://localhost:1234.(только для разработки)✅ Подключение к комнате "general " на https://localhost:1234...
Первые два сообщения предназначены только для разработки. В режиме разработки React всегда повторно монтирует каждый компонент один раз.
React проверяет, что ваш эффект может повторно синхронизироваться, заставляя его сделать это немедленно в режиме разработки. Это может напомнить вам открытие двери и ее дополнительное закрытие, чтобы проверить, работает ли замок. React запускает и останавливает ваш эффект еще один раз в режиме разработки, чтобы проверить вы правильно реализовали его очистку.
Основная причина, по которой ваш эффект будет пересинхронизироваться на практике, — это изменение некоторых используемых им данных. В приведенном выше песочнице измените выбранный чат-рум. Обратите внимание, как при изменении roomId ваш эффект пересинхронизируется.
Однако существуют и более необычные случаи, в которых пересинхронизация необходима. Например, попробуйте отредактировать serverUrl в приведенном выше примере, пока чат открыт. Обратите внимание, как эффект пересинхронизируется в ответ на ваши изменения в коде. В будущем React может добавить больше функций, которые будут полагаться на пересинхронизацию.
Как React узнает, что ему нужно пересинхронизировать эффект
Возможно, вам интересно, как React узнал, что вашему эффекту нужно пересинхронизироваться после изменения roomId. Это потому, что вы сообщили React, что его код зависит от roomId, включив его в список зависимостей:
function ChatRoom({ roomId }) { // Проп roomId может изменяться со временем
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Этот эффект считывает roomId
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]); // Таким образом, вы сообщаете React, что этот эффект «зависит» от roomId
// ...Вот как это работает:
- Вы знали, что
roomId— это проп, а значит, он может меняться со временем. - Вы знали, что ваш эффект считывает
roomId(то есть его логика зависит от значения, которое может измениться позже). - Именно поэтому вы указали его в качестве зависимости вашего эффекта (чтобы он пересинхронизировался при изменении
roomId) .
Каждый раз после повторного рендеринга вашего компонента React будет проверять массив зависимостей, который вы передали. Если какое-либо значение в массиве отличается от значения в том же месте, которое вы передали во время предыдущего рендеринга, React повторно синхронизирует ваш эффект.
Например, если вы передали [«general»] во время первоначального рендеринга, а позже передали [«travel»] во время следующего рендеринга, React сравнит «general» и «travel». Это разные значения (по сравнению с Object.is), поэтому React повторно синхронизирует ваш эффект. С другой стороны, если ваш компонент перерисовывается, но roomId не изменился, ваш эффект останется подключенным к той же комнате.
Каждый Effect представляет собой отдельный процесс синхронизации
Не добавляйте в ваш Effect постороннюю логику только потому, что эта логика должна выполняться одновременно с уже написанным вами Effect. Например, предположим, вы хотите отправить аналитическое событие, когда пользователь заходит в комнату. У вас уже есть Effect, зависящий от roomId, поэтому у вас может возникнуть соблазн добавить туда вызов аналитики:
function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}Но представьте, что позже вы добавите в этот эффект еще одну зависимость, для которой потребуется восстановить соединение. Если этот эффект пересинхронизируется, он также вызовет logVisit(roomId) для той же комнаты, чего вы не хотели. Регистрация посещения является отдельным процессом от установки соединения. Напишите их как два отдельных эффекта:
function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
}, [roomId]);
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
// ...
}, [roomId]);
// ...
}** Каждый эффект в вашем коде должен представлять собой отдельный и независимый процесс синхронизации.**
В приведенном выше примере удаление одного эффекта не нарушит логику другого эффекта. Это хороший признак того, что они синхронизируют разные вещи, и поэтому было разумно разделить их. С другой стороны, если вы разделите связный фрагмент логики на отдельные эффекты, код может выглядеть « чище», но будет сложнее в обслуживании. Вот почему вам следует думать о том, являются ли процессы одинаковыми или отдельными, а не о том, выглядит ли код чище.
Эффекты «реагируют» на реактивные значения
Ваш эффект считывает две переменные (serverUrl и roomId), но вы указали в качестве зависимости только roomId:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}Почему serverUrl не нужно указывать в качестве зависимости?
Это потому, что serverUrl никогда не меняется в результате повторного рендеринга. Он всегда остается неизменным, независимо от того, сколько раз и по какой причине компонент перерисовывается. Поскольку serverUrl никогда не меняется, не имеет смысла указывать его в качестве зависимости. Ведь зависимости выполняют какие-то действия только тогда, когда они меняются со временем!
С другой стороны, roomId может отличаться при повторном рендеринге. *Пропсы, состояние и другие значения, объявленные внутри компонента, являются реактивными, поскольку они вычисляются во время рендеринга и участвуют в потоке данных React.
Если бы serverUrl был переменной состояния, он был бы реактивным. Реактивные значения должны быть включены в зависимости:
function ChatRoom({ roomId }) { // Пропсы меняются со временем
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // Состояние может меняться со временем
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Ваш эффект считывает пропсы и состояние
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // Таким образом, вы сообщаете React, что этот эффект «зависит» от пропсов и состояния
// .. .
}Включив serverUrl в качестве зависимости, вы гарантируете, что эффект пересинхронизируется после его изменения.
Попробуйте изменить выбранный чат-рум или отредактировать URL-адрес сервера в этой песочнице:
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js' ;
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
return (
<>
<label>
URL сервера:{' '}
<input
value={serverUrl}
onChange={e => setServerUrl(e.target.value)}
/>
</label>
<h1>Добро пожаловать в комнату {roomId}!</h1>
</>
);
}
export default function App() {
const [roomId, setRoomId] = useState('general');
return (
<>
<label>
Выберите чат-комнату:{' '}
<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 или serverUrl, эффект повторно подключается к серверу чата.
Что означает эффект с пустыми зависимостями
Что произойдет , если вы вынесете и serverUrl, и roomId за пределы компонента?
const serverUrl = 'https://localhost:1234';
const roomId = 'general';
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ Все зависимости объявлены
// ...
}Теперь код вашего эффекта не использует никаких реактивных значений, поэтому его зависимости могут быть пустыми ([]).
Если смотреть с точки зрения компонента, пустой массив зависимостей [] означает, что этот эффект подключается к чату только при монтировании компонента и отключается только при его демонтировании. (Имейте в виду, что React все равно проведет дополнительную ресинхронизацию во время разработки, чтобы провести стресс-тестирование вашей логики.)
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
const serverUrl = 'https://localhost:1234';
const roomId = 'general';
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []);
return <h1>Добро пожаловать в комнату {roomId}!</h1>;
}
export default function App() {
const [show, setShow] = useState(false);
return (
<>
<button onClick={() => setShow(!show)}>
{show ? 'Закрыть чат' : 'Открыть чат'}
</button>
{show && <hr />}
{show && <ChatRoom />}
</>
);
}Однако, если вы думаете с точки зрения эффекта, вам вообще не нужно думать о подключении и отключении. Важно то, что вы указали, что делает ваш эффект для начала и остановки синхронизации. На сегодняшний день у него нет реактивных зависимостей. Но если вы когда-нибудь захотите, чтобы пользователь мог изменять roomId или serverUrl со временем (и они станут реактивными), код вашего эффекта не изменится. Вам нужно будет только добавить их в список зависимостей.
Все переменные, объявленные в теле компонента, являются реактивными
Пропсы и состояние — не единственные реактивные значения. Значения, которые вы вычисляете на их основе, также являются реактивными. Если пропсы или состояние изменятся, ваш компонент перерисуется, и вычисленные на их основе значения также изменятся. Именно поэтому все переменные из тела компонента, используемые эффектом, должны находиться в списке зависимостей эффекта.
Допустим, что пользователь может выбрать сервер чата в раскрывающемся списке, но также может настроить сервер по умолчанию в настройках. Предположим, вы уже поместили состояние настроек в [контекст](/learn/scaling-up -with-reducer-and-context), поэтому вы читаете settings из этого контекста. Теперь вы вычисляете serverUrl на основе выбранного сервера из props и сервера по умолчанию:
function ChatRoom({ roomId, selectedServerUrl }) { // roomId является реактивным
const settings = useContext(SettingsContext); // settings является реактивным
const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; // serverUrl является реактивным
useEffect(() => {
const connection = createConnection(serverUrl, roomId); / / Ваш эффект считывает roomId и serverUrl
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // Поэтому ему нужно повторно синхронизироваться, когда изменяется любой из них!
// ...
}В этом примере serverUrl не является пропсом или переменной состояния. Это обычная переменная, которую вы вычисляете во время рендеринга. Но она вычисляется во время рендеринга, поэтому может измениться в результате повторного рендеринга. Вот почему она является реактивной.
Все значения внутри компонента (включая props, state и переменные в теле вашего компонента) являются реактивными. Любое реактивное значение может измениться при повторном рендеринге, поэтому вам нужно включать реактивные значения в качестве зависимостей Effect.
Другими словами, Effects «реагируют» на все значения из тела компонента.
Могут ли глобальные или изменяемые значения быть зависимостями?
Изменяемые значения (включая глобальные переменные) не являются реактивными.
Изменяемое значение, такое как location.pathname, не может быть зависимостью. Оно изменяемо, поэтому может измениться в любой момент совершенно вне потока данных рендеринга React. Его изменение не вызовет повторного рендеринга вашего компонента. Поэтому, даже если вы указали его в зависимостях, React *
не будет знать*, что нужно повторно синхронизировать эффект при его изменении. Это также нарушает правила React, поскольку чтение изменяемых данных во время рендеринга (то есть в момент вычисления зависимостей) нарушает чистоту рендеринга. Вместо этого вам следует считывать и подписываться на внешнее изменяемое значение с помощью [useSyncExternalStore.] (/learn/you-might-not-need-an-effect#subscribing-to-an-external-store)
** Изменяемое значение, такое как ref.current, или то, что вы из него считываете, также не может быть зависимостью.** Сам объект ref, возвращаемый useRef, может быть зависимостью, но его свойство current намеренно сделано изменяемым. Это позволяет вам [отслеживать что-то, не вызывая повторного рендеринга.] (/learn/referencing-values-with-refs) Но поскольку его изменение не вызывает перерисовку, оно не является реактивным значением, и React не будет знать, что нужно запустить ваш эффект заново, когда оно изменится.
Как вы узнаете ниже на этой странице, линтер будет автоматически проверять наличие этих проблем.
React проверяет, что вы указали каждое реактивное значение в качестве зависимости
Если ваш линтер настроен для React, он проверит, что каждое реактивное значение, используемое кодом вашего эффекта, объявлен в качестве его зависимости. Например, это ошибка линтера, поскольку и roomId, и serverUrl являются реактивными:
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) { // roomId является реактивным
const [serverUrl, setServerUrl] = useState('https://localhost:1234') ; // serverUrl является реактивным
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // <-- Здесь что-то не так!
return (
<>
<label>
URL сервера:{' '}
<input
value={serverUrl}
onChange={e => setServerUrl(e.target.value)}
/>
</label>
<h1>Добро пожаловать в комнату {roomId}!</h1>
</>
);
}
export default function App() {
const [roomId, setRoomId] = useState('general');
return (
<>
<label>
Выберите чат-комнату:{' '}
<select
value={roomId}
onChange={e => setRoomId(e.target.value)}
>
<option value="general">общий</option>
<option value="travel">путешествия</option>
<option value="music">музыка</option>
</select>
</label>
<hr />
<ChatRoom roomId={roomId} />
</>
);
}Это может выглядеть как ошибка React, но на самом деле React указывает на ошибку в вашем коде. Как roomId, так и serverUrl могут меняться со временем, но вы забываете -синхронизировать ваш Effect при их изменении. Вы останетесь подключенными к исходным roomId и serverUrl даже после того, как пользователь выберет другие значения в интерфейсе.
Чтобы исправить ошибку, следуйте рекомендации линтера и укажите roomId и serverUrl в качестве зависимостей вашего Effect:
function ChatRoom({ roomId }) { // roomId является реактивным
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl является реактивным
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]); // ✅ Все зависимости объявлены
// ...
}Попробуйте это исправление в песочнице выше. Убедитесь, что ошибка линтера исчезла, а чат подключается заново при необходимости.
В некоторых случаях React знает, что значение никогда не изменяется, даже если оно объявлено внутри компонента. Например, функция set, возвращаемая из useState, и объект ref, возвращаемый useRef, являются стабильными — гарантируется, что они не изменятся при повторном рендеринге. Стабильные значения не являются реактивными, поэтому вы можете опустить их из списка. Включать их разрешается: они не изменятся, поэтому это не имеет значения .
Что делать, если вы не хотите повторно синхронизировать
В предыдущем примере вы исправили ошибку линтера, указав roomId и serverUrl в качестве зависимостей.
Однако вместо этого вы могли бы «доказать» линкеру, что эти значения не являются реактивными, т. е. что они не могут измениться в результате повторного рендеринга. Например, если serverUrl и roomId не зависят от рендеринга и всегда имеют одинаковые значения, вы можете вынести их за пределы компонента. Теперь они не должны быть зависимостями:
{1,2,11}
const serverUrl = 'https://localhost:1234'; // serverUrl не является реактивным
const roomId = 'general'; // roomId не является реактивным
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ Все зависимости объявлены
// ...
}Вы также можете переместить их внутрь эффекта. Они не вычисляются во время рендеринга, поэтому не являются реактивными:
function ChatRoom() {
useEffect(() => {
const serverUrl = 'https://localhost:1234'; // serverUrl не является реактивным
const roomId = 'general'; // roomId не является реактивным
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ Все зависимости объявлены
// ...
}Эффекты — это реактивные блоки кода. Они повторно синхронизируются, когда значения, которые вы считываете внутри них, изменяются. В отличие от обработчиков событий, которые запускаются только один раз за взаимодействие, эффекты запускаются всякий раз, когда требуется синхронизация.
Вы не можете «выбирать» свои зависимости. Ваши зависимости должны включать все [реактивные значения](# все-переменные-объявленные-в-теле-компонента-являются-реактивными), которые вы считываете в эффекте. Линтер обеспечивает соблюдение этого правила. Иногда это может привести к таким проблемам, как бесконечные циклы и слишком частая пересинхронизация вашего эффекта. Не исправляйте эти проблемы, отключая линтер! Вот что следует попробовать вместо этого:
-
Убедитесь, что ваш эффект представляет собой независимый процесс синхронизации. Если ваш Effect ничего не синхронизирует, он может быть ненужным. Если он синхронизирует несколько независимых вещей, разделите его.
-
Если вы хотите прочитать последнее значение props или state, не «реагируя» на него и не пересинхронизируя Effect, вы можете разделить свой эффект на реактивную часть (которую вы оставите в эффекте) и нереактивную часть (которую вы выделите в так называемое событие эффекта). Узнайте о разделении событий и эффектов.
-
Старайтесь не полагаться на объекты и функции в качестве зависимостей. Если вы создаете объекты и функции во время рендеринга, а затем считываете их из Effect, они будут отличаться при каждом рендеринге. Это приведет к тому, что ваш Effect будет пересинхронизироваться каждый раз. Узнайте больше об удалении ненужных зависимостей из Effects.
Линтер — ваш друг, но его возможности ограничены. Линтер знает только, когда зависимости * неправильные*. Он не знает лучшего способа решения каждого конкретного случая. Если линтер предлагает зависимость, но её добавление приводит к циклу, это не значит, что линтер следует игнорировать. Вам нужно изменить код внутри (или вне) эффекта так, чтобы это значение не было реактивным и не требовало быть зависимостью.
Если у вас есть существующая кодовая база, у вас могут быть некоторые эффекты, которые подавляют линтер следующим образом:
useEffect(() => {
// ...
// 🔴 Избегайте подавления линтера таким образом:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);На следующих страницах вы узнаете, как исправить этот код, не нарушая правил. Это всегда стоит исправить!
Вывод
- Компоненты могут монтироваться, обновляться и демонтироваться.
- Каждый эффект имеет жизненный цикл, отдельный от окружающего компонента.
- Каждый эффект описывает отдельный процесс синхронизации, который может запускаться и останавливаться.
- При написании и чтении эффектов думайте с точки зрения каждого отдельного эффекта (как запустить и остановить синхронизацию), а не с точки зрения компонента (как он монтируется, обновляется или демонтируется).
- Значения, объявленные внутри тела компонента, являются «реактивными».
- Реактивные значения должны -синхронизироваться с эффектом, поскольку они могут меняться со временем.
- Линтер проверяет, что все реактивные значения, используемые внутри эффекта, указаны в качестве зависимостей.
- Все ошибки, отмеченные линтером, являются обоснованными. Всегда есть способ исправить код, чтобы он не нарушал правил.
Возможно, вам не нужен эффект
Эффекты — это «лазейка» из парадигмы React. Они позволяют вам «выйти за пределы» React и синхронизировать ваши компоненты с какой-либо внешней системой, такой как виджет, не относящийся к React, сеть или DOM браузера. Если никаких внешних систем не задействовано (например, если вы хотите обновить состояние компонента при изменении каких-либо пропсов или состояния), вам не нужен эффект. Удаление ненужных эффектов сделает ваш код более понятным, быстрым в исполнении и менее подверженным ошибкам.
Разделение событий и эффектов
Обработчики событий запускаются повторно только тогда, когда вы выполняете то же взаимодействие снова. В отличие от обработчиков событий, эффекты синхронизируются заново, если какое-либо значение, которое они считывают (например, проп или переменная состояния), отличается от того, что было во время последнего рендеринга. Иногда вам также может понадобиться сочетание обоих типов поведения- эффект, который запускается повторно в ответ на одни значения, но не на другие. На этой странице вы узнаете, как это сделать.