useEffect
useEffect — это React-хук, который позволяет вам синхронизировать компонент с внешней системой.
useEffect(setup, dependencies?)
- Справочник
-
Использование
- Подключение к внешней системе
- Оборачивание эффектов в пользовательские хуки
- Управление виджетом, не являющимся React-виджетом
- Получение данных с помощью эффектов
- Определение реактивных зависимостей
- Обновление состояния на основе предыдущего состояния из эффекта
- Удаление ненужных зависимостей от объектов
- Удаление ненужных функциональных зависимостей
- Чтение последних свойств и состояния из эффекта
- Отображение разного контента на сервере и клиенте
-
Отладка
- Мой эффект выполняется дважды при монтировании компонента
- Мой эффект выполняется после каждого перерисовки
- Мой эффект продолжает выполняться в бесконечном цикле
- Логика очистки выполняется, хотя мой компонент не размонтирован
- Мой эффект выполняет визуальное действие, и я вижу мерцание перед его выполнением
Справочник
useEffect(setup, dependencies?)
Вызовите useEffect на верхнем уровне вашего компонента, чтобы объявить эффект:
import { 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();
};
}, [serverUrl, roomId]);
// ...
} Параметры
-
setup: Функция с логикой вашего эффекта. Ваша функция инициализации также может необязательно возвращать функцию очистки. Когда ваш компонент добавляется в DOM, React выполнит вашу функцию инициализации. После каждой перерисовки с изменёнными зависимостями React сначала выполнит функцию очистки (если вы её предоставили) со старыми значениями, а затем выполнит вашу функцию инициализации с новыми значениями. После того, как ваш компонент удален из DOM, React выполнит вашу функцию очистки. -
необязательный
dependencies: Список всех реактивных значений, используемых внутри кодаsetup. Реактивные значения включают свойства, состояние и все переменные и функции, объявленные непосредственно внутри тела вашего компонента. Если ваш линтер настроен для React, он проверит, что каждое реактивное значение корректно указано в качестве зависимости. Список зависимостей должен иметь постоянное количество элементов и быть записан в строку, как[dep1, dep2, dep3]. React будет сравнивать каждую зависимость со своим предыдущим значением, используя сравнениеObject.is. Если вы опустите этот аргумент, ваш эффект будет повторно выполняться после каждой перерисовки компонента. См. разницу между передачей массива зависимостей, пустого массива и отсутствием зависимостей.
Возвращает
useEffect возвращает undefined.
Ограничения
-
useEffect— это хук, поэтому вы можете вызывать его только на верхнем уровне вашего компонента или ваших собственных хуков. Вы не можете вызывать его внутри циклов или условных операторов. Если вам это нужно, создайте новый компонент и перенесите состояние в него. -
Если вы не пытаетесь синхронизироваться с какой-либо внешней системой, вам, вероятно, не нужен эффект.
-
Когда включён режим Strict Mode, React выполнит один дополнительный цикл инициализации + очистки для разработки перед первым реальным циклом. Это тест на стресс, который гарантирует, что ваша логика очистки «отражает» логику инициализации, и что она останавливает или отменяет то, что делает функция инициализации. Если это вызывает проблему, реализуйте функцию очистки.
-
Если некоторые из ваших зависимостей — объекты или функции, определённые внутри компонента, существует риск, что они вызовут повторное выполнение эффекта чаще, чем нужно. Чтобы исправить это, удалите ненужные объектные и функциональные зависимости. Вы также можете извлечь обновления состояния и нереактивную логику за пределами вашего эффекта.
-
Если ваш эффект не был вызван взаимодействием (например, нажатием), React, как правило, позволит браузеру сначала отрисовать обновлённый экран, прежде чем выполнить ваш эффект. Если ваш эффект выполняет визуальное действие (например, позиционирование подсказки), и задержка заметна (например, она мерцает), замените
useEffectнаuseLayoutEffect. -
Если ваш эффект вызван взаимодействием (например, нажатием), React может выполнить ваш эффект перед тем, как браузер отрисует обновлённый экран. Это гарантирует, что результат эффекта может быть замечен системой событий. Обычно это работает как ожидается. Однако, если вы должны отложить работу до отрисовки, например,
alert(), вы можете использоватьsetTimeout. См. reactwg/react-18/128 для получения дополнительной информации. -
Даже если ваш эффект был вызван взаимодействием (например, нажатием), React может позволить браузеру перерисовать экран перед обработкой обновлений состояния внутри вашего эффекта. Обычно это работает как ожидается. Однако, если вы должны заблокировать браузер от перерисовки экрана, вам нужно заменить
useEffectнаuseLayoutEffect. -
Эффекты выполняются только на клиенте. Они не выполняются во время серверной отрисовки.
Использование
Подключение к внешней системе
Некоторые компоненты должны оставаться подключёнными к сети, к некоторым API браузера или к библиотекам третьих сторон, пока они отображаются на странице. Эти системы не управляются React, поэтому они называются внешними.
Чтобы подключить свой компонент к какой-либо внешней системе, вызовите useEffect на верхнем уровне вашего компонента:
import { 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();
};
}, [serverUrl, roomId]);
// ...
} Вам нужно передать два аргумента в useEffect:
- Функцию инициализации с кодом инициализации, который подключается к этой системе.
- Она должна возвращать функцию очистки с кодом очистки, который отключает от этой системы.
- Список зависимостей, включающий все значения из вашего компонента, используемые внутри этих функций.
React вызывает ваши функции инициализации и очистки всякий раз, когда это необходимо, что может произойти несколько раз:
- Ваш код инициализации выполняется, когда ваш компонент добавляется на страницу (монтируется).
- После каждой перерисовки вашего компонента, где изменились зависимости:
- Сначала выполняется ваш код очистки со старыми свойствами и состоянием.
- Затем выполняется ваш код инициализации с новыми свойствами и состоянием.
- Ваш код очистки выполняется один последний раз после удаления вашего компонента со страницы (размонтирование).
Давайте проиллюстрируем эту последовательность для примера выше.
Когда компонент ChatRoom добавляется на страницу, он подключится к чату с начальными serverUrl и roomId. Если serverUrl или roomId изменяются в результате перерисовки (например, если пользователь выбирает другой чат в выпадающем списке), ваш эффект отключится от предыдущей комнаты и подключится к следующей. Когда компонент ChatRoom удаляется со страницы, ваш эффект отключится ещё раз.
Чтобы помочь вам найти ошибки, в режиме разработки React выполняет инициализацию и очистку один раз дополнительно перед инициализацией. Это тест на стресс, который проверяет, что ваша логика эффекта реализована правильно. Если это вызывает видимые проблемы, вашей функции очистки не хватает некоторой логики. Функция очистки должна останавливать или отменять то, что делала функция инициализации. Правило заключается в том, что пользователь не должен быть способен отличить между вызовом инициализации один раз (как в производстве) и последовательностью инициализация → очистка → инициализация (как в разработке). См. общие решения.
Постарайтесь создать каждый эффект как независимый процесс и думать о единственном цикле инициализации/очистки за раз. Не должно иметь значения, монтируется ли ваш компонент, обновляется или размонтируется. Когда ваша логика очистки правильно «отражает» логику инициализации, ваш эффект устойчив к выполнению инициализации и очистки так часто, как необходимо.
Примечание
Эффект позволяет вам синхронизировать свой компонент с какой-либо внешней системой (например, с сервисом чата). Здесь под внешней системой понимается любой фрагмент кода, который не контролируется React, например:
- Таймер, управляемый с помощью
setInterval()иclearInterval(). - Подписка на событие с помощью
window.addEventListener()иwindow.removeEventListener(). - Библиотека сторонних анимаций с API, подобным
animation.start()иanimation.reset().
Если вы не подключаетесь к какой-либо внешней системе, вам, вероятно, не нужен эффект.
Оборачивание эффектов в пользовательские хуки
Эффекты — это “скрытый механизм”: вы используете их, когда вам нужно “выйти за пределы React” и когда для вашей задачи нет лучшего встроенного решения. Если вам часто приходится вручную писать эффекты, это обычно означает, что вам нужно извлечь некоторые пользовательские хуки для распространенных действий, от которых зависят ваши компоненты.
Например, этот useChatRoom пользовательский хук “скрывает” логику вашего эффекта за более декларативным API:
function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
} Затем вы можете использовать его в любом компоненте следующим образом:
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ... В экосистеме React также доступно множество отличных пользовательских хуков для любых целей.
Узнайте больше об обёртке эффектов в пользовательских хуках.
Управление виджетом, не основанным на React
Иногда вы хотите синхронизировать внешнюю систему с некоторым свойством или состоянием вашего компонента.
Например, если у вас есть виджет карты стороннего разработчика или компонент видеоплеера, написанный без React, вы можете использовать эффект для вызова методов, которые соответствуют состоянию внешней системы текущему состоянию вашего компонента React. Этот эффект создаёт экземпляр класса MapWidget , определённого в map-widget.js. При изменении свойства zoomLevel компонента Map эффект вызывает метод setZoom() экземпляра класса, чтобы поддерживать синхронизацию:
import { useRef, useEffect } from 'react'; import { MapWidget } from './map-widget.js'; export default function Map({ zoomLevel }) { const containerRef = useRef(null); const mapRef = useRef(null); useEffect(() => { if (mapRef.current === null) { mapRef.current = new MapWidget(containerRef.current); } const map = mapRef.current; map.setZoom(zoomLevel); }, [zoomLevel]); return ( <div style={{ width: 200, height: 200 }} ref={containerRef} /> ); }
В этом примере функция очистки не требуется, поскольку класс MapWidget управляет только узлом DOM, который был ему передан. После удаления компонента React Map из дерева как узел DOM, так и экземпляр класса MapWidget автоматически удаляются сборщиком мусора JavaScript-движка браузера.
Получение данных с помощью эффектов
Вы можете использовать эффект для получения данных для вашего компонента. Обратите внимание, что если вы используете фреймворк, использование механизма получения данных вашего фреймворка будет намного эффективнее, чем ручное создание эффектов.
Если вы хотите вручную получить данные из эффекта, ваш код может выглядеть следующим образом:
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';
export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);
useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
};
}, [person]);
// ... Обратите внимание на переменную ignore, которая инициализируется значением false, и устанавливается в значение true во время очистки. Это гарантирует что ваш код не страдает от "конфликтов гонок": ответы сети могут поступать в другом порядке, чем вы их отправляли.
import { useState, useEffect } from 'react'; import { fetchBio } from './api.js'; export default function Page() { const [person, setPerson] = useState('Alice'); const [bio, setBio] = useState(null); useEffect(() => { let ignore = false; setBio(null); fetchBio(person).then(result => { if (!ignore) { setBio(result); } }); return () => { ignore = true; } }, [person]); return ( <> <select value={person} onChange={e => { setPerson(e.target.value); }}> <option value="Alice">Alice</option> <option value="Bob">Bob</option> <option value="Taylor">Taylor</option> </select> <hr /> <p><i>{bio ?? 'Loading...'}</i></p> </> ); }
Вы также можете переписать код, используя синтаксис async / await, но при этом вам по-прежнему необходимо указать функцию очистки:
import { useState, useEffect } from 'react'; import { fetchBio } from './api.js'; export default function Page() { const [person, setPerson] = useState('Alice'); const [bio, setBio] = useState(null); useEffect(() => { async function startFetching() { setBio(null); const result = await fetchBio(person); if (!ignore) { setBio(result); } } let ignore = false; startFetching(); return () => { ignore = true; } }, [person]); return ( <> <select value={person} onChange={e => { setPerson(e.target.value); }}> <option value="Alice">Alice</option> <option value="Bob">Bob</option> <option value="Taylor">Taylor</option> </select> <hr /> <p><i>{bio ?? 'Loading...'}</i></p> </> ); }
Прямое получение данных в эффектах становится повторяющимся и затрудняет добавление оптимизаций, таких как кэширование и рендеринг на сервере в дальнейшем. Проще использовать пользовательский хук — либо свой собственный, либо поддерживаемый сообществом.
Подробное описание
Какие хорошие альтернативы получению данных в эффектах?
Написание fetch вызовов внутри эффектов — популярный способ получения данных, особенно в приложениях, работающих исключительно на стороне клиента. Однако это очень ручное подходом и имеет значительные недостатки:
- Эффекты не выполняются на сервере. Это означает, что первоначальный рендер HTML на сервере будет содержать только состояние загрузки без данных. Компьютер клиента должен загрузить весь JavaScript и отрисовать приложение только для того, чтобы узнать, что теперь ему необходимо загрузить данные. Это не очень эффективно.
- Прямое получение данных в эффектах легко приводит к “водопадам сети”. Вы отображаете родительский компонент, он получает некоторые данные, отображает дочерние компоненты, и затем они начинают получать свои данные. Если сеть не очень быстрая, это значительно медленнее, чем одновременное получение всех данных.
- Прямое получение данных в эффектах, как правило, означает, что вы не предварительно загружаете или не кэшируете данные. Например, если компонент отключается, а затем снова подключается, ему снова придётся получать данные.
-
Это не очень удобно. При написании вызовов
fetchтаким образом, чтобы не возникало ошибок, как, например, "конфликты гонок", затрачивается значительный объём кода.
Этот список недостатков не специфичен для React. Он применим к получению данных при монтировании с любой библиотекой. Как и с маршрутизацией, получение данных не является тривиальной задачей, поэтому мы рекомендуем следующие подходы:
- Если вы используете фреймворк, используйте встроенный механизм получения данных. Современные фреймворки React имеют встроенные механизмы получения данных, которые эффективны и не страдают от перечисленных выше недостатков.
- В противном случае рассмотрите возможность использования или создания кэша на стороне клиента. Популярные решения с открытым исходным кодом включают React Query, useSWR и React Router 6.4+. Вы также можете создать собственное решение, в котором вы будете использовать эффекты внутри, но также добавите логику для дублирования запросов, кэширования ответов и предотвращения водопадов сети (путем предварительной загрузки данных или повышения требований к данным до маршрутов).
Вы можете продолжать получать данные непосредственно в эффектах, если ни один из этих подходов вам не подходит.
Указание реактивных зависимостей
Обратите внимание, что вы не можете “выбрать” зависимости вашего эффекта. Каждое реактивное значение, используемое в коде вашего эффекта, должно быть объявлено как зависимость. Список зависимостей вашего эффекта определяется окружающим кодом:
function ChatRoom({ roomId }) { // This is a reactive value
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // This is a reactive value too
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads these reactive values
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ So you must specify them as dependencies of your Effect
// ...
} Если значение serverUrl или roomId изменится, ваш эффект подключится к чату с новыми значениями.
Реактивные значения включают свойства и все переменные и функции, объявленные непосредственно внутри вашего компонента. Поскольку roomId и serverUrl являются реактивными значениями, вы не можете удалить их из зависимостей. Если вы попытаетесь их опустить, и ваш линтер правильно настроен для React, линтер отобразит эту ошибку, которую нужно исправить:
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect has missing dependencies: 'roomId' and 'serverUrl'
// ...
} Чтобы удалить зависимость, необходимо “доказать” линтеру, что она не нужна в качестве зависимости. Например, вы можете перенести serverUrl за пределы вашего компонента, чтобы доказать, что она не является реактивной и не будет изменяться при повторном рендеринге:
const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
} Теперь serverUrl не является реактивным значением (и не может измениться при повторном рендеринге), и не нуждается в качестве зависимости. Если код вашего эффекта не использует реактивных значений, его список зависимостей должен быть пустым ([]):
const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
const roomId = 'music'; // Not a reactive value anymore
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...
} Эффект с пустыми зависимостями не выполняется повторно, когда изменяются какие-либо свойства или состояние вашего компонента.
Опасная ситуация
Если у вас есть существующий код, у вас могут быть некоторые эффекты, которые подавляют линтер следующим образом:
useEffect(() => {
// ...
// 🔴 Avoid suppressing the linter like this:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);
Когда зависимости не соответствуют коду, высока вероятность возникновения ошибок. Подавляя линтер, вы “обманываете” React относительно значений, от которых зависит ваш эффект. Вместо этого докажите, что они не нужны.
Обновление состояния на основе предыдущего состояния из эффекта
Когда вы хотите обновить состояние на основе предыдущего состояния из эффекта, у вас могут возникнуть проблемы:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // You want to increment the counter every second...
}, 1000)
return () => clearInterval(intervalId);
}, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval.
// ...
} Поскольку count является реактивным значением, оно должно быть указано в списке зависимостей. Однако это заставляет эффект очищаться и настраиваться заново каждый раз, когда изменяется count. Это нежелательно.
Для исправления этой проблемы, передайте функцию обновления состояния c => c + 1 в setCount:
import { useState, useEffect } from 'react'; export default function Counter() { const [count, setCount] = useState(0); useEffect(() => { const intervalId = setInterval(() => { setCount(c => c + 1); // ✅ Pass a state updater }, 1000); return () => clearInterval(intervalId); }, []); // ✅ Now count is not a dependency return <h1>{count}</h1>; }
Теперь, когда вы передаёте c => c + 1 вместо count + 1, ваш эффект больше не должен зависеть от count. В результате этого исправления ему не нужно будет очищать и настраивать интервал каждый раз, когда изменяется count.
Удаление ненужных зависимостей объектов
Если ваш эффект зависит от объекта или функции, созданных во время отрисовки, он может выполняться слишком часто. Например, этот эффект подключается снова после каждой отрисовки, потому что объект options отличается при каждой отрисовке:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = { // 🚩 This object is created from scratch on every re-render
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options); // It's used inside the Effect
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 As a result, these dependencies are always different on a re-render
// ... Избегайте использования объекта, созданного во время отрисовки, в качестве зависимости. Вместо этого создайте объект внутри эффекта:
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} /> </> ); }
Теперь, когда вы создаёте объект options внутри эффекта, сам эффект зависит только от строки roomId.
С этим исправлением написание в поле ввода не приводит к повторному подключению чата. В отличие от объекта, который создаётся заново, строка, например, roomId, не меняется, пока вы не присвоите ей другое значение. Узнайте больше о удалении зависимостей.
Удаление ненужных зависимостей от функций
Если ваш эффект зависит от объекта или функции, созданных во время отрисовки, он может выполняться слишком часто. Например, этот эффект подключается снова после каждой отрисовки, потому что функция createOptions отличается при каждой отрисовке:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() { // 🚩 This function is created from scratch on every re-render
return {
serverUrl: serverUrl,
roomId: roomId
};
}
useEffect(() => {
const options = createOptions(); // It's used inside the Effect
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🚩 As a result, these dependencies are always different on a re-render
// ... Само по себе создание функции с нуля при каждой повторной отрисовке — не проблема. Вам не нужно этого оптимизировать. Однако, если вы используете её в качестве зависимости вашего эффекта, это приведёт к повторному запуску эффекта после каждой повторной отрисовки.
Избегайте использования функции, созданной во время отрисовки, в качестве зависимости. Вместо этого объявите её внутри эффекта:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; 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]); 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} /> </> ); }
Теперь, когда вы определяете функцию createOptions внутри эффекта, сам эффект зависит только от строки roomId. С этим исправлением написание в поле ввода не приводит к повторному подключению чата. В отличие от функции, которая создаётся заново, строка, например, roomId, не меняется, пока вы не присвоите ей другое значение. Узнайте больше о удалении зависимостей.
Чтение последних props и состояния из эффекта
В разработке
Этот раздел описывает экспериментальный API, который ещё не выпущен в стабильной версии React.
По умолчанию, когда вы читаете реактивное значение из эффекта, вам нужно добавить его в качестве зависимости. Это гарантирует, что ваш эффект «реагирует» на каждое изменение этого значения. Для большинства зависимостей это желаемое поведение.
Однако иногда вам нужно прочитать последние props и состояние из эффекта, не «реагируя» на них. Например, представьте, что вы хотите записать количество элементов в корзине покупок при каждом посещении страницы:
function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ All dependencies declared
// ...
} А что, если вы хотите записывать новое посещение страницы после каждого изменения url, но не если изменяется только shoppingCart? Вы не можете исключить shoppingCart из зависимостей без нарушения правил реактивности. Однако вы можете указать, что вам не нужно, чтобы фрагмент кода «реагировал» на изменения, даже если он вызывается изнутри эффекта. Объявите событие эффекта с помощью хука useEffectEvent и перенесите код чтения shoppingCart внутрь него:
function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
} События эффектов не являются реактивными и всегда должны быть исключены из зависимостей вашего эффекта. Это позволяет помещать нереактивный код (где вы можете прочитать последнее значение некоторых props и состояния) внутрь них. Читая shoppingCart внутри onVisit, вы гарантируете, что shoppingCart не запустит ваш эффект повторно.
Узнайте больше о том, как события эффектов позволяют вам разделить реактивный и нереактивный код.
Отображение разного контента на сервере и клиенте
Если ваше приложение использует серверную отрисовку (либо непосредственно, либо через фреймворк), ваш компонент будет отрисовываться в двух разных средах. На сервере он будет отрисовываться для создания начального HTML. На клиенте React запустит код отрисовки снова, чтобы он мог подключить ваши обработчики событий к этому HTML. Именно поэтому для гидратации начальный вывод отрисовки должен быть идентичным на клиенте и сервере.
В редких случаях вам может потребоваться отобразить разный контент на клиенте. Например, если ваше приложение считывает данные из localStorage, то это невозможно сделать на сервере. Вот как можно это реализовать:
function MyComponent() {
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
}, []);
if (didMount) {
// ... return client-only JSX ...
} else {
// ... return initial JSX ...
}
} Пока приложение загружается, пользователь увидит начальный вывод отрисовки. Затем, когда оно загрузится и будет гидратировано, ваш эффект выполнится и установит didMount в true, вызвав повторную отрисовку. Это переключит отображение на клиентский вывод. Эффекты не выполняются на сервере, поэтому didMount был false во время начальной серверной отрисовки.
Используйте этот шаблон экономно. Имейте в виду, что пользователи со слабым соединением будут видеть начальный контент довольно долго — потенциально несколько секунд — поэтому вы не хотите вносить резкие изменения в отображение вашего компонента. Во многих случаях вы можете избежать этого, условно отображая разные вещи с помощью CSS.
Отладка
Мой эффект выполняется дважды при подключении компонента
При включенном режиме строгости в режиме разработки React выполняет настройку и очистку на одну дополнительную итерацию перед фактической настройкой.
Это тест на прочность, который проверяет правильность реализации логики вашего эффекта. Если это приводит к видимым проблемам, ваша функция очистки пропускает какой-то логический блок. Функция очистки должна остановить или отменить то, что делала функция настройки. Правило гласит, что пользователь не должен различать между выполнением настройки один раз (как в продакшене) и последовательностью настройка → очистка → настройка (как в режиме разработки).
Узнайте больше о том, как это помогает найти ошибки и как исправить свою логику.
Мой эффект выполняется после каждой повторной отрисовки
Сначала проверьте, не забыли ли вы указать массив зависимостей:
useEffect(() => {
// ...
}); // 🚩 No dependency array: re-runs after every render! Если вы указали массив зависимостей, но ваш эффект всё ещё выполняется циклически, это происходит потому, что одна из ваших зависимостей отличается при каждой повторной отрисовке.
Вы можете отладить эту проблему, вручную выведя свои зависимости в консоль:
useEffect(() => {
// ..
}, [serverUrl, roomId]);
console.log([serverUrl, roomId]); Затем вы можете щелкнуть правой кнопкой мыши по массивам из разных повторных отрисовок в консоли и выбрать «Сохранить как глобальную переменную» для обоих. Предположим, первый был сохранён как temp1, а второй — как temp2, затем вы можете использовать консоль браузера, чтобы проверить, одинаковы ли каждая зависимость в обоих массивах:
Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ... Найдя зависимость, которая отличается при каждой повторной отрисовке, вы обычно можете исправить её одним из следующих способов:
- Обновление состояния на основе предыдущего состояния из эффекта
- Удаление ненужных зависимостей от объектов
- Удаление ненужных зависимостей от функций
- Чтение последних props и состояния из эффекта
В качестве последнего средства (если эти методы не помогли), оберните его создание с помощью useMemo или useCallback (для функций).
Мой эффект продолжает выполняться в бесконечном цикле
Если ваш эффект выполняется в бесконечном цикле, должны выполняться эти два условия:
- Ваш эффект обновляет некоторое состояние.
- Это состояние приводит к повторной отрисовке, которая меняет зависимости эффекта.
Перед тем, как начать исправление проблемы, спросите себя, подключён ли ваш эффект к какой-либо внешней системе (например, DOM, сети, виджету стороннего разработчика и т. д.). Зачем вашему эффекту нужно устанавливать состояние? Синхронизируется ли он с этой внешней системой? Или вы пытаетесь управлять потоком данных вашего приложения с его помощью?
Если внешней системы нет, подумайте, не упростила ли бы логику полная отмена эффекта. Удалить эффект.
Если вы действительно синхронизируетесь с какой-то внешней системой, подумайте, почему и при каких условиях ваш эффект должен обновлять состояние. Что-то изменилось, что влияет на визуальный вывод вашего компонента? Если вам нужно отслеживать какие-то данные, которые не используются для отрисовки, ссылка ref (которая не вызывает повторные отрисовки) может быть более подходящей. Убедитесь, что ваш эффект не обновляет состояние (и не вызывает повторные отрисовки) чаще, чем необходимо.
Наконец, если ваш эффект обновляет состояние в нужное время, но цикл всё ещё существует, это происходит потому, что это обновление состояния приводит к изменению одной из зависимостей эффекта. Прочитайте о том, как отладить изменения зависимостей.
Логика очистки выполняется, хотя мой компонент не был удалён
Функция очистки запускается не только при размонтировании, но и перед каждой повторной отрисовкой при изменении зависимостей. Кроме того, в режиме разработки React выполняет настройку+очистку дополнительно сразу после подключения компонента.
Если у вас есть код очистки без соответствующего кода настройки, это обычно признак ошибки:
useEffect(() => {
// 🔴 Avoid: Cleanup logic without corresponding setup logic
return () => {
doSomething();
};
}, []); Ваша логика очистки должна быть «симметричной» логике настройки и должна остановить или отменить то, что сделала настройка:
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]); Узнайте о том, как жизненный цикл эффекта отличается от жизненного цикла компонента.
Мой эффект выполняет визуальные действия, и я вижу мерцание перед его выполнением
Если ваш Эффект должен заблокировать браузер от отрисовки экрана, замените useEffect на useLayoutEffect. Обратите внимание, что это не нужно для подавляющего большинства Эффектов. Вам потребуется это только в том случае, если крайне важно выполнить ваш Эффект перед отрисовкой браузера: например, чтобы измерить и расположить всплывающую подсказку перед тем, как пользователь её увидит.
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://18.react.dev/reference/react/useEffect