Вы написали несколько компонентов с использованием хуков. Возможно — даже создали небольшое приложение. В целом результат вас вполне устраивает. Вы привыкли к API и в процессе работы обнаружили несколько неочевидных полезных приёмов. Вы даже создали несколько собственных хуков и сократили свой код на 300 строк, поместив в них то, что раньше было представлено повторяющимися фрагментами программы. То, что вы сделали, вы показали коллегам. «Отлично получилось», — сказали они о вашем проекте.
Но иногда, когда вы используете useEffect
, составные части программных механизмов не особенно хорошо стыкуются друг с другом. Вам кажется, что вы что-то упускаете. Всё это похоже на работу с событиями жизненного цикла компонентов, основанных на классах… но так ли это на самом деле?
Пытаясь понять — что именно вас не устраивает, вы замечаете, что задаётесь следующими вопросами:
- Как воспроизвести
componentDidMount
с помощьюuseEffect
? - Как правильно загружать данные внутри
useEffect
? Что такое[]
? - Нужно ли указывать функции в виде зависимостей эффектов?
- Почему иногда программа попадает в бесконечный цикл повторной загрузки данных?
- Почему иногда внутри эффектов видно старое состояние или встречаются старые свойства?
Когда я только начал использовать хуки, меня тоже мучили эти вопросы. Даже когда я готовил документацию, я не мог бы сказать, что в совершенстве владею некоторыми тонкостями. С тех пор у меня было несколько моментов, когда я, вдруг поняв что-то важное, прямо-таки хотел воскликнуть: «Эврика!». О том, что я в эти моменты осознал, я и хочу вам рассказать. То, что вы узнаете сейчас о useEffect
, позволит вам совершенно чётко разглядеть очевидные ответы на вышеприведённые вопросы.
Но для того чтобы увидеть ответы на эти вопросы, нам сначала надо сделать шаг назад. Цель этой статьи не в том, чтобы дать её читателям некую пошаговую инструкцию по работе с useEffect
. Она нацелена на то, чтобы помочь вам, что называется, «грокнуть» useEffect
. И, честно говоря, тут не так много всего нужно изучить. На самом деле, большую часть времени мы потратим на забывание того, что знали раньше.
У меня в голове всё сошлось только после того, как я перестал смотреть на хук useEffect
через призму знакомых мне методов жизненного цикла компонентов, основанных на классах.
«Ты должен забыть то, чему тебя учили»
habr.com/ru/company/ruvds/blog/445276/Йода
Предполагается, что читатель этого материала в определённой степени знаком с API useEffect. Это довольно длинная статья, её можно сравнить с небольшой книгой. Дело в том, что я предпочитаю выражать свои мысли именно так. Ниже, очень кратко, приведены ответы на те вопросы, о которых речь шла выше. Пожалуй, они пригодятся тем, у кого нет времени или желания читать весь материал.
Если тот формат, в котором мы собираемся рассмотреть useEffect
, со всеми его объяснениями и примерами, вам не очень подходит, вы можете немного подождать — до того момента, когда эти объяснения появятся в бесчисленном множестве других руководств. Тут — та же история, что и с самой библиотекой React, которая в 2013 году была чем-то совершенно новым. Для того чтобы сообщество разработчиков распознало бы новую ментальную модель и чтобы появились бы учебные материалы, основанные на этой модели, нужно некоторое время.
Ответы на вопросы
Вот краткие ответы на вопросы, поставленные в начале этого материала, предназначенные для тех, кто не хочет читать весь этот текст. Если, читая эти ответы, вы почувствуете, что не очень понимаете смысл прочитанного — полистайте материал. В тексте вы найдёте подробные пояснения. Если же вы собираетесь прочесть всё — этот раздел можете пропустить.
▍Как воспроизвести componentDidMount с помощью useEffect?
Хотя для воспроизведения функционала componentDidMount
можно воспользоваться конструкцией useEffect(fn, [])
, она не является точным эквивалентом componentDidMount
. А именно, она, в отличие от componentDidMount
, захватывает свойства и состояние. Поэтому, даже внутри коллбэка, вы будете видеть исходные свойства и состояние. Если вы хотите увидеть самую свежую версию чего-либо, это можно записать в ссылку ref
. Но обычно существует более простой способ структурирования кода, поэтому делать это необязательно. Помните о том, что ментальная модель эффектов отличается от той, что применима к componentDidMount
и к другим методам жизненного цикла компонентов. Поэтому попытка найти точные эквиваленты может принести больше вреда, чем пользы. Для того чтобы работать продуктивно, нужно, так сказать, «думать эффектами». Основа их ментальной модели ближе к реализации синхронизации, чем к реагированию на события жизненного цикла компонентов.
▍Как правильно загружать данные внутри useEffect? Что такое []?
Вот хорошее руководство по загрузке данных с использованием useEffect
. Постарайтесь прочитать его целиком! Оно не такое большое, как это. Скобки, []
, представляющие пустой массив, означают, что эффект не использует значения, участвующие в потоке данных React, и по этой причине безопасным можно считать его однократное применение. Кроме того, использование пустого массива зависимостей является обычным источником ошибок в том случае, если некое значение, на самом деле, используется в эффекте. Вам понадобится освоить несколько стратегий (преимущественно, представленных в виде useReducer
и useCallback
), которые могут помочь устранить необходимость в зависимости вместо того, чтобы необоснованно эту зависимость отбрасывать.
▍Нужно ли указывать функции в виде зависимостей эффектов?
Рекомендовано выносить за пределы компонентов те функции, которые не нуждаются в свойствах или в состоянии, а те функции, которые используются только эффектами, рекомендуется помещать внутрь эффектов. Если после этого ваш эффект всё ещё пользуется функциями, находящимися в области видимости рендера (включая функции из свойств), оберните их в useCallback
там, где они объявлены, и попробуйте снова ими воспользоваться. Почему это важно? Функции могут «видеть» значения из свойств и состояния, поэтому они принимают участие в потоке данных. Вот более подробные сведения об этом в нашем FAQ.
▍Почему иногда программа попадает в бесконечный цикл повторной загрузки данных?
Это может происходить тогда, когда загрузка данных выполняется в эффекте, у которого нет второго аргумента, представляющего зависимости. Без него эффекты выполняются после каждой операции рендеринга — а это значит, что установка состояния приведёт к повторному вызову таких эффектов. Бесконечный цикл может возникнуть и в том случае, если в массиве зависимостей указывают значение, которое всегда изменяется. Выяснить — что это за значение можно, удаляя зависимости по одной. Однако, удаление зависимостей (или необдуманное использование []
) — это обычно неправильный подход к решению проблемы. Вместо этого стоит найти источник проблемы и решить её по-настоящему. Например, подобную проблему могут вызывать функции. Помочь решить её можно, помещая их в эффекты, вынося их за пределы компонентов, или оборачивая в useCallback
. Для того чтобы избежать многократного создания объектов, можно воспользоваться useMemo
.
▍Почему иногда внутри эффектов видно старое состояние или встречаются старые свойства?
Эффекты всегда «видят» свойства и состояние из рендера, в котором они объявлены. Это помогает предотвращать ошибки, но в некоторых случаях может и помешать нормальной работе компонента. В таких случаях можно для работы с такими значениями в явном виде использовать мутабельные ссылки ref
(почитать об этом можно в конце вышеупомянутой статьи). Если вы думаете, что видите свойства или состояние из старого рендера, но этого не ожидаете, то вы, возможно, упустили какие-то зависимости. Для того чтобы приучиться их видеть, воспользуйтесь этим правилом линтера. Через пару дней это станет чем-то вроде вашей второй натуры. Кроме того, взгляните на этот ответ в нашем FAQ.
Надеюсь, эти ответы на вопросы оказались полезными тем, кто их прочитал. А теперь давайте подробнее поговорим о useEffect
.
У каждого рендера есть собственные свойства и состояние
Прежде чем мы сможем обсуждать эффекты, нам надо поговорить о рендеринге.
Вот функциональный компонент-счётчик.
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Внимательно присмотритесь к строке <p>You clicked {count} times</p>
. Что она означает? «Наблюдает» ли каким-то образом константа count
за изменениями в состоянии и обновляется ли она автоматически? Такое заключение можно считать чем-то вроде ценной первой идеи того, кто изучает React, но оно не является точной ментальной моделью происходящего.
В нашем примере count
— это просто число. Это не некая магическая «привязка данных», не некий «объект-наблюдатель» или «прокси», или что угодно другое. Перед нами — старое доброе число, вроде этого:
const count = 42;
// ...
<p>You clicked {count} times</p>
// ...
Во время первого вывода компонента значение count
, получаемое из useState()
, равняется 0. Когда мы вызываем setCount(1)
, React снова вызывает компонент. В этот раз count
будет равно 1. И так далее:
// Во время первого рендеринга
function Counter() {
const count = 0; // Возвращено useState()
// ...
<p>You clicked {count} times</p>
// ...
}
// После щелчка наша функция вызывается снова
function Counter() {
const count = 1; // Возвращено useState()
// ...
<p>You clicked {count} times</p>
// ...
}
// После ещё одного щелчка функция вызывается снова
function Counter() {
const count = 2; // Возвращено useState()
// ...
<p>You clicked {count} times</p>
// ...
}
React вызывает компонент всякий раз, когда мы обновляем состояние. В результате каждая операция рендеринга «видит» собственное значение состояния counter
, которое, внутри функции, является константой.
В результате эта строка не выполняет какую-то особую операцию привязки данных:
<p>You clicked {count} times</p>
Она лишь встраивает числовое значение в код, формируемый при рендеринге. Это число предоставляется средствами React. Когда мы вызываем setCount
, React снова вызывает компонент с другим значением count
. Затем React обновляет DOM для того чтобы объектная модель документа соответствовала бы самым свежим данным, выведенным в ходе рендеринга компонента.
Самый главный вывод, который можно из этого сделать, заключается в том, что count
является константой внутри любого конкретного рендера и со временем не меняется. Меняется компонент, который вызывается снова и снова. Каждый рендер «видит» собственное значение count
, которое оказывается изолированным для каждой из операций рендеринга.
В этом материале можно найти подробности о данном процессе.
У каждого рендера имеются собственные обработчики событий
До сих пор всё понятно. А что можно сказать об обработчиках событий?
Взгляните на этот пример. Здесь, через три секунды после нажатия на кнопку, выводится окно сообщения со сведениями о значении, хранящемся в count
:
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
Предположим, я выполню следующую последовательность действий:
- Доведу значение
count
до 3, щёлкая по кнопкеClick me
. - Щёлкну по кнопке
Show alert
. - Увеличу значение до 5 до истечения таймаута.
Увеличение значения count после щелчка по кнопке Show alert
Как вы думаете, что выведется в окне сообщения? Будет ли там выведено 5, что соответствует значению count
на момент срабатывания таймера, или 3 — то есть значение count
в момент нажатия на кнопку?
Сейчас вы узнаете ответ на этот вопрос, но, если хотите выяснить всё сами — вот рабочая версия этого примера.
Если то, что вы увидели, кажется вам непонятным — вот вам пример, который ближе к реальности. Представьте себе приложение-чат, в котором, в состоянии, хранится ID
текущего получателя сообщения, и имеется кнопка Send
. В этом материале происходящее рассматривается в подробностях. Собственно говоря, правильным ответом на вопрос о том, что появится в окне сообщения, является 3.
Механизм вывода окна сообщения «захватил» состояние в момент щелчка по кнопке.
Есть способы реализовать и другой вариант поведения, но мы пока будем заниматься стандартным поведением системы. При построении ментальных моделей технологий важно отличать «путь наименьшего сопротивления» от всяческих «запасных выходов».
Как же всё это работает?
Мы уже говорили о том, что значение count
является константой для каждого конкретного вызова нашей функции. Полагаю, стоит остановиться на этом подробнее. Речь идёт о том, что наша функция вызывается много раз (один раз на каждую операцию рендеринга), но при каждом из этих вызовов count
внутри неё является константой. Эта константа установлена в некое конкретное значение (представляющее собой состояние конкретной операции рендеринга).
Подобное поведение функций не является чем-то особенным для React — обычные функции ведут себя похожим образом:
function sayHi(person) {
const name = person.name;
setTimeout(() => {
alert('Hello, ' + name);
}, 3000);
}
let someone = {name: 'Dan'};
sayHi(someone);
someone = {name: 'Yuzhi'};
sayHi(someone);
someone = {name: 'Dominic'};
sayHi(someone);
В этом примере внешняя переменная someone
несколько раз переназначается. Такое же может произойти и где-то внутри React, текущее состояние компонента может меняться. Однако внутри функции sayHi
имеется локальная константа name
, которая связана с person
из конкретного вызова. Эта константа является локальной, поэтому её значения в разных вызовах функции изолированы друг от друга! В результате, по прошествии тайм-аута, каждое выводимое окно сообщения «помнит» собственное значение name
.
Это объясняет то, как наш обработчик события захватывает значение count
в момент щелчка по кнопке. Если мы, работая с компонентами, применим тот же принцип, то окажется, что каждый рендер «видит» собственное значение count
:
// Во время первого рендеринга
function Counter() {
const count = 0; // Возвращено useState()
// ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
// ...
}
// После щелчка наша функция вызывается снова
function Counter() {
const count = 1; // Возвращено useState()
// ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
// ...
}
// После ещё одного щелчка функция вызывается снова
function Counter() {
const count = 2; // Возвращено useState()
// ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
// ...
}
В результате каждый рендер, фактически, возвращает собственную «версию» handleAlertClick
. Каждая из таких версий «помнит» собственное значение count
:
// Во время первого рендеринга
function Counter() {
// ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + 0);
}, 3000);
}
// ...
<button onClick={handleAlertClick} /> // Версия, хранящая значение 0
// ...
}
// После щелчка наша функция вызывается снова
function Counter() {
// ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + 1);
}, 3000);
}
// ...
<button onClick={handleAlertClick} /> // Версия, хранящая значение 1
// ...
}
// После ещё одного щелчка функция вызывается снова
function Counter() {
// ...
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + 2);
}, 3000);
}
// ...
<button onClick={handleAlertClick} /> // Версия, хранящая значение 2
// ...
}
Именно поэтому в этом примере обработчики событий «принадлежат» конкретным рендерам, а когда вы щёлкаете по кнопке, компонент использует состояние count
из этих рендеров.
Внутри каждого конкретного рендера свойства и состояние всегда остаются одними и теми же. Но если в разных операциях рендеринга используются собственные свойства и состояние, то же самое происходит и с любыми механизмами, использующими их (включая обработчики событий). Они тоже «принадлежат» конкретным рендерам. Поэтому даже асинхронные функции внутри обработчиков событий будут «видеть» те же самые значения count
.
Надо отметить, что в вышеприведённом примере я встроил конкретные значения count
прямо в функции handleAlertClick
. Эта «мысленная» замена нам не повредит, так как константа count
не может изменяться в пределах конкретного рендера. Во-первых, это константа, во вторых — это число. Можно с уверенностью говорить о том, что так же можно размышлять и о других значениях, вроде объектов, но только в том случае, если мы примем за правило не выполнять изменения (мутации) состояния. При этом нас устраивает вызов setSomething(newObj)
с новым объектом вместо изменения существующего, так как при таком подходе состояние, принадлежащее предыдущему рендеру, оказывается нетронутым.
У каждого рендера есть собственные эффекты
Этот материал, как вы знаете, посвящён эффектам, но мы пока ещё о них даже не говорили. Сейчас мы это исправим. Как оказывается, работа с эффектами не особенно отличается от того, с чем мы уже разобрались.
Рассмотрим пример из документации, который очень похож на тот, который мы уже разбирали:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Теперь у меня к вам вопрос. Как эффект считывает самое свежее значение count
?
Может быть, тут используется некая «привязка данных», или «объект-наблюдатель», который обновляет значение count
внутри функции эффекта? Может быть count
— это мутабельная переменная, значение которой React устанавливает внутри нашего компонента, в результате чего эффект всегда видит её самую свежую версию?
Нет.
Мы уже знаем, что в рендере конкретного компонента count
представляет собой константу. Даже обработчики событий «видят» значение count
из рендера, которому они «принадлежат» из-за того, что count
— это константа, находящаяся в определённой области видимости. То же самое справедливо и для эффектов!
И надо отметить, что это не переменная count
каким-то образом меняется внутри «неизменного» эффекта. Перед нами — сама функция эффекта, различная в каждой операции рендеринга.
Каждая версия «видит» значение count
из рендера, к которому она «принадлежит»:
// Во время первого рендеринга
function Counter() {
// ...
useEffect(
// Функция эффекта из первого рендера
() => {
document.title = `You clicked ${0} times`;
}
);
// ...
}
// После щелчка наша функция вызывается снова
function Counter() {
// ...
useEffect(
// Функция эффекта из второго рендера
() => {
document.title = `You clicked ${1} times`;
}
);
// ...
}
// После ещё одного щелчка функция вызывается снова
function Counter() {
// ...
useEffect(
// Функция эффекта из третьего рендера
() => {
document.title = `You clicked ${2} times`;
}
);
// ..
}
React запоминает предоставленную нами функцию эффекта, выполняет её после сброса значений в DOM и позволяет браузеру вывести изображение на экран.
В результате, даже если мы говорим здесь о единственном концептуальном эффекте (обновляющем заголовок документа), он, в каждом рендере, представлен новой функцией, а каждая функция эффекта «видит» свойства и состояние из конкретного рендера, которому она «принадлежит».
Эффект, концептуально, можно представить в качестве части результатов рендеринга.
Строго говоря, это не так (для того, чтобы сделать возможной композицию хуков без необходимости пользоваться неуклюжим синтаксисом или без создания чрезмерной нагрузки на систему). Но в ментальной модели, созданием которой мы тут занимаемся, функции эффектов принадлежат конкретному рендеру точно так же, как обработчики событий.
Для того чтобы убедиться в том, что мы всё это как следует поняли, давайте рассмотрим ещё раз нашу первую операцию рендеринга:
React:
- Дай мне пользовательский интерфейс в условиях, когда в состояние записано число 0.
Компонент:
- Вот результаты рендеринга:
<p>You clicked 0 times</p>
. - Кроме того, не забудь выполнить этот эффект после того, как завершишь работу:
() => { document.title = 'You clicked 0 times' }
.
React:
- Конечно. Обновляю интерфейс. Эй, браузер, я добавляю кое-что в DOM.
Браузер:
- Отлично, я вывел это на экран.
React:
- Хорошо, теперь я запущу эффект, который мне дал компонент.
- Выполняю
() => { document.title = 'You clicked 0 times' }
.
А теперь давайте разберём то, что происходит после щелчка по кнопке. На самом деле, многое тут повторяет предыдущий разбор, но кое-что здесь выглядит иначе:
Компонент:
- Эй, React, установи моё состояние в 1.
React:
- Дай мне пользовательский интерфейс в условиях, когда в состояние записано число 1.
Компонент:
- Вот результаты рендеринга:
<p>You clicked 1 times</p>
. - Кроме того, не забудь выполнить этот эффект после того, как завершишь работу:
() => { document.title = 'You clicked 1 times' }
.
React:
- Конечно. Обновляю интерфейс. Эй, браузер, я изменил кое-что в DOM.
Браузер:
- Отлично, я вывел изменения на экран.
React:
- Хорошо, теперь я запущу эффект, который мне дал компонент.
- Выполняю
() => { document.title = 'You clicked 1 times' }
.
Каждому рендеру принадлежит… всё
Теперь мы знаем о том, что эффекты, выполняемые после каждой операции рендеринга, концептуально являются частью вывода компонента, и «видят» свойства и состояние из этой конкретной операции.
Попробуем выполнить мысленный эксперимент. Рассмотрим следующий код:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Что будет выведено в консоль в том случае, если быстро щёлкнуть по кнопке несколько раз?
Как обычно, сейчас мы рассмотрим ответ на этот вопрос. Возможно, вам сейчас может показаться, что это простая задачка, и результат работы этого кода интуитивно понятен. Но это не так! Мы увидим последовательность операций, выполняющих вывод в консоль, каждая из которых принадлежит конкретному рендеру, и, в результате, пользуется собственным значением count
. Попробуйте поэкспериментировать с этим примером сами.
Щелчки по кнопке и вывод данных в консоль
Тут вы можете подумать: «Конечно, именно так это и работает! Да и может ли эта программа вести себя иначе?».
Ну, на самом деле, this.setState
в компонентах, основанных на классах, работает не так. Поэтому легко допустить ошибку, если полагать, что следующий вариант примера, в котором используется компонент, основанный на классе, эквивалентен предыдущему:
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
Дело в том, что this.state.count
всегда указывает на самое свежее значение count
, а не на значение, принадлежащее конкретному рендеру. В результате, вместо последовательности сообщений с разными числами, мы, быстро щёлкнув по кнопке 5 раз, увидим 5 одинаковых сообщений.
Щелчки по кнопке и вывод данных в консоль
Я вижу иронию в том, что хуки так сильно полагаются на JavaScript-замыкания, а компоненты, основанные на классах, страдают от традиционной проблемы, связанной с неправильным значением, которое попадает в коллбэк функции setTimeout
, которую часто считаю обычной для замыканий. Дело в том, что истинным источником проблемы в этом примере является мутация (React выполняет изменение this.state
в классах таким образом, чтобы это значение указывало бы на самую свежую версию состояния), а не механизм замыканий.
Замыкания — это отличный инструмент в том случае, если значение, которое «запирают» в замыкании, никогда не меняется. Это облегчает их использование и размышления о них, так как, в сущности, речь идёт о константах. И, как мы уже говорили, свойства и состояние никогда не меняются в конкретном рендере. Да, кстати, версию этого примера, в которой используются компоненты, основанные на классах, можно исправить, воспользовавшись замыканием.
Плывём против течения
Сейчас нам важно отметить, что каждая функция внутри механизма рендеринга компонента (включая обработчики событий, эффекты, тайм-ауты или вызовы API внутри них) захватывает свойства и состояние вызова рендера, который их определил.
В результате следующие два компонента эквивалентны:
function Example(props) {
useEffect(() => {
setTimeout(() => {
console.log(props.counter);
}, 1000);
});
// ...
}
function Example(props) {
const counter = props.counter;
useEffect(() => {
setTimeout(() => {
console.log(counter);
}, 1000);
});
// ...
}
При этом неважно, выполняется ли внутри компонента «заблаговременное» чтение из свойств или состояния. Они не изменятся! Внутри области видимости отдельно взятого рендера свойства и состояния не изменяются. Надо отметить, что деструктурирование свойств делает это более очевидным.
Конечно, иногда, внутри какого-нибудь коллбэка, объявленного в эффекте, нужно прочитать самое свежее значение, а не то, что было захвачено. Легче всего это сделать, используя ссылки ref
, почитать об этом можно в последнем разделе этой статьи.
Учитывайте то, что когда вам нужно прочитать будущие свойства или состояние из функции, принадлежащей ранее выполненной операции рендеринга, то вы, так сказать, пытаетесь плыть против течения. Нельзя сказать, что это неправильно (и иногда это просто необходимо), но менее «чистым» решением может показаться выход за рамки традиционной парадигмы React-разработки. Такой шаг может вести к ожидаемым последствиям, так как это помогает лучше увидеть то, какие фрагменты кода являются ненадёжными и зависящими от таймингов. Когда подобное происходит при работе с классами, это оказывается менее очевидным.
Вот версия нашего примера со счётчиком щелчков, основанная на функции, которая воспроизводит поведение той его версии, которая основана на классе:
function Example() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect(() => {
// Установить мутабельное значение в самое свежее состояние count
latestCount.current = count;
setTimeout(() => {
// Прочитать мутабельное значение с самыми свежими данными
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
});
// ...
Щелчки по кнопке и вывод данных в консоль
Заниматься изменениями чего-либо в React может показаться странной идеей. Однако именно так сам React переназначает значение this.state
в классах. В отличие от работы с захваченными свойствами и состоянием, у нас нет никакой гарантии того, что чтение latestCount.current
даст один и тот же результат в разных коллбэках. По определению, менять это значение можно в любое время. Именно поэтому этот механизм не применяется по умолчанию, и для того, чтобы им воспользоваться, нужно сделать осознанный выбор.
Как насчёт очистки?
Как поясняется в документации, некоторые эффекты могут иметь фазу очистки. В сущности, цель этой операции заключается в том, чтобы «отменять» действия эффектов для вариантов их применения наподобие оформления подписок.
Рассмотрим этот код:
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
};
});
Предположим, props
— это объект {id: 10}
в первой операции рендеринга, и {id: 20}
— во второй. Можно подумать, что тут происходит примерно следующее:
- React выполняет очистку эффекта для
{id: 10}
. - React рендерит интерфейс для
{id: 20}
. - React выполняет эффект для
{id: 20}
.
(Но это, на самом деле, не совсем так.)
Пользуясь этой ментальной моделью можно подумать, что операция очистки «видит» старые свойства из-за того, что она выполняется до повторного рендеринга, после чего новый эффект «видит» новые свойства из-за того, что он выполняется после повторного рендеринга. Это — ментальная модель, которая базируется на методах жизненного цикла компонентов, основанных на классах, и здесь она не позволяет добиться точных результатов. Поговорим о причинах этого несоответствия.
React выполняет эффекты только после того, как позволит браузеру вывести изображение на экран. Это ускоряет приложение, так как большинству эффектов не нужно блокировать обновления экрана. Очистка эффекта также откладывается. Предыдущий эффект входит в стадию очистки после повторного рендеринга с новыми свойствами. В результате мы выходим на следующую последовательность действий:
- React рендерит интерфейс для
{id: 20}
. - Браузер выводит изображение на экран. Пользователь видит интерфейс для
{id: 20}
. - React выполняет очистку эффекта для
{id: 10}
. - React выполняет эффект для
{id: 20}
.
Тут вы можете задаться вопросом о том, как операция очистки предыдущего эффекта всё ещё может видеть «старое» значение props
, содержащее {id: 10}
, после того, как в props
записано {id: 20}
.
Надо отметить, что мы уже здесь были…
А может это — та же самая кошка?
Приведём цитату из предыдущего раздела: «каждая функция внутри механизма рендеринга компонента (включая обработчики событий, эффекты, тайм-ауты или вызовы API внутри них) захватывает свойства и состояние вызова рендера, который их определил».
Теперь ответ очевиден! В ходе операции очистки эффекта не производится чтение «самых свежих» свойств, что бы это ни значило. Эта операция читает свойства, которые принадлежат рендеру, в котором они определены:
// Первый рендер, в props записано {id: 10}
function Example() {
// ...
useEffect(
// Эффект из первого рендера
() => {
ChatAPI.subscribeToFriendStatus(10, handleStatusChange);
// Очистка для эффекта из первого рендера
return () => {
ChatAPI.unsubscribeFromFriendStatus(10, handleStatusChange);
};
}
);
// ...
}
// Следующий рендер, в props записано {id: 20}
function Example() {
// ...
useEffect(
// Эффект из второго рендера
() => {
ChatAPI.subscribeToFriendStatus(20, handleStatusChange);
// Очистка для эффекта из второго рендера
return () => {
ChatAPI.unsubscribeFromFriendStatus(20, handleStatusChange);
};
}
);
// ...
}
Королевства будут расти и превращаться в пепел, Солнце сбросит внешние оболочки и станет белым карликом, последняя цивилизация исчезнет… Но ничто не заставит свойства, которые «увидела» операция очистки эффекта из первого рендеринга, превратиться во что-то, отличающееся от {id: 10}
.
Именно это позволяет React работать с эффектами сразу после вывода изображения на экран. Это, без дополнительных усилий со стороны программиста, делает его приложения быстрее. Если нашему коду понадобятся старые значения props
, они никуда не деваются.
Синхронизация, а не жизненный цикл
Одной из моих любимых особенностей React является то, что эта библиотека унифицирует описание результатов первого рендеринга компонента и обновлений. Это уменьшает энтропию программ.
Предположим, мой компонент выглядит так:
function Greeting({ name }) {
return (
<h1 className="Greeting">
Hello, {name}
</h1>
);
}
При его использовании совершенно неважно, будет ли сначала отрендерено <Greeting name="Dan" />
, а потом — <Greeting name="Yuzhi" />
, или если компонент просто сразу выведет <Greeting name="Yuzhi" />
. И в том и в другом случаях в итоге мы увидим текст Hello, Yuzhi
.
Говорят, что важен путь, а не цель. Если говорить о React, то справедливым окажется обратное утверждение. Здесь важна цель, а не то, каким путём к ней идут. В этом и заключается разница между вызовами вида $.addClass
и $.removeClass
в jQuery-коде (это — то, что мы называем «путём»), и указание того, каким должен быть CSS-класс в React (то есть — того, какой должна быть «цель»).
React синхронизирует DOM с тем, что имеется в текущих свойствах и состоянии. При рендеринге нет разницы между «монтированием» и «обновлением».
Об эффектах стоит размышлять в похожем ключе. Использование useEffect
позволяет синхронизировать сущности, находящиеся за пределами дерева React, со свойствами и состоянием.
function Greeting({ name }) {
useEffect(() => {
document.title = 'Hello, ' + name;
});
return (
<h1 className="Greeting">
Hello, {name}
</h1>
);
}
В этом состоит незначительное отличие восприятия useEffect
от привычной ментальной модели, в которую входят понятия монтирования, обновления и размонтирования компонентов. Если вы пытаетесь создать эффект, который ведёт себя по-особому при первом рендеринге компонента, то вы пытаетесь плыть против течения! Синхронизация не удастся в том случае, если наш результат зависит от «пути», а не от «цели».
Не должно быть разницы между тем, выполняем ли мы рендеринг компонента сначала со свойством A
, потом с B
, а потом — со свойством C
, и той ситуацией, когда мы сразу же рендерим его со свойством C
. Хотя в процессе работы этих двух вариантов кода и могут быть некоторые временные различия (например, возникающие при загрузке каких-либо данных), в итоге конечный результат должен быть тем же самым.
Надо отметить, что, конечно, выполнение эффекта при каждой операции рендеринга может быть неэффективным вариантом решения некоей задачи. (А в некоторых случаях это может привести к бесконечным циклам).
Как с этим бороться?
Учим React различать эффекты
Мы уже научили React разборчивости при работе с DOM. Вместо того чтобы касаться DOM при каждой операции повторного рендеринга компонента, React обновляет лишь те части DOM, которые по-настоящему меняются.
Предположим, у нас есть такой код:
<h1 className="Greeting">
Hello, Dan
</h1>
Мы хотим обновить его до такого состояния:
<h1 className="Greeting">
Hello, Yuzhi
</h1>
React видит два объекта:
const oldProps = {className: 'Greeting', children: 'Hello, Dan'};
const newProps = {className: 'Greeting', children: 'Hello, Yuzhi'};
React просматривает свойства этих объектов и выясняет, что значение children
изменилось, для его вывода на экран нужно обновление DOM. При этом оказывается, что className
осталось неизменным. Поэтому можно просто поступить так:
domNode.innerText = 'Hello, Yuzhi';
// domNode.className трогать не нужно
Можем ли мы сделать что-то подобное этому и с эффектами? Было бы очень хорошо, если можно было бы избежать их повторного запуска в тех случаях, когда в их применении нет необходимости.
Например, возможно, компонент выполняет повторный рендеринг из-за изменения состояния:
function Greeting({ name }) {
const [counter, setCounter] = useState(0);
useEffect(() => {
document.title = 'Hello, ' + name;
});
return (
<h1 className="Greeting">
Hello, {name}
<button onClick={() => setCounter(counter + 1)}>
Increment
</button>
</h1>
);
}
Но эффект не использует значение counter
из состояния. Эффект синхронизирует document.title
со свойством name
, но свойство name
тут не меняется. Перезапись document.title
при каждом изменении counter
кажется решением, далёким от идеального.
Может ли React просто… сравнить эффекты?
let oldEffect = () => { document.title = 'Hello, Dan'; };
let newEffect = () => { document.title = 'Hello, Dan'; };
// Может ли React увидеть то, что эти функции делают одно и то же?
На самом деле — нет. React не может догадаться о том, что именно делает функция, не вызывая её. (Исходный код не содержит конкретных значений. Он просто включает в себя свойство name
.)
Именно поэтому, если нужно избежать ненужных перезапусков эффектов, эффекту можно передать массив зависимостей (такие массивы ещё называют deps
), выглядящий как аргумент useEffect
:
useEffect(() => {
document.title = 'Hello, ' + name;
}, [name]); // Наши зависимости
Это похоже на то, как если бы мы сказали React: «Слушай, я понимаю, что внутрь этой функции ты заглянуть не можешь, но я обещаю, что я будут использовать только name
и ничего другого из области видимости рендера».
Если окажется так, что зависимости после предыдущего вызова эффекта не менялись, то эффекту нечего будет синхронизировать и React может выполнение этого эффекта пропустить:
const oldEffect = () => { document.title = 'Hello, Dan'; };
const oldDeps = ['Dan'];
const newEffect = () => { document.title = 'Hello, Dan'; };
const newDeps = ['Dan'];
// React не может заглянуть в функцию, но он может сравнить зависимости.
// Так как значения зависимостей остались прежними, новый эффект вызывать не нужно.
Если же хотя бы одно значение из массива зависимостей изменится, то мы будем знать, что при очередном выполнении рендеринга вызов эффекта пропустить нельзя! Ведь иначе ни о какой синхронизации чего-либо с чем-либо не может быть и речи.
Не лгите React о зависимостях
Если утаить от React правду о зависимостях — это будет иметь плохие последствия. Интуитивно понятно, что это так, но мне довелось наблюдать за тем, что практически все люди, которые пытались пользоваться useEffect
, полагаясь на сложившуюся у них ментальную модель компонентов, основанных на классах, пытаются обойти правила. (И я поначалу поступал точно так же!)
function SearchResults() {
async function fetchData() {
// ...
}
useEffect(() => {
fetchData();
}, []); // Нормально ли это? Не всегда. Есть лучшие способы написания такого кода.
// ...
}
FAQ по хукам даёт пояснения по поводу того, что тут правильно будет сделать. Мы вернёмся к этому примеру позже.
«Но я хочу запустить эффект только при монтировании!», — скажете вы. Пока запомните: если вы указываете зависимости, то в массиве должны быть представлены все значения из компонента, которые используются эффектом. Сюда входят свойства, состояние, функции, то есть — всё, что находится в компоненте и используется эффектом.
Иногда, когда вы так поступаете, это вызывает проблемы. Например, может быть, вы сталкиваетесь с бесконечным циклом загрузки данных, или с тем, что слишком часто пересоздаются сокеты. Решение этой проблемы заключается не в том, чтобы избавиться от зависимостей. Скоро мы это обсудим.
Но, прежде чем мы перейдём к решению, давайте лучше вникнем в суть проблемы.
Что происходит в том случае, когда зависимости лгут React
Если зависимости содержат абсолютно все значения, используемые эффектом, то React знает о том, когда этот эффект нужно перезапустить.
useEffect(() => {
document.title = 'Hello, ' + name;
}, [name]);
Так как зависимости различаются — эффект перезапускается
Но если мы для этого эффекта укажем, в качестве зависимостей, пустой массив, []
, тогда, при обновлении данных, используемых в эффекте, он перезапущен не будет:
useEffect(() => {
document.title = 'Hello, ' + name;
}, []); // Неправильно: в зависимостях нет name
Зависимости выглядят одинаково — эффект повторно не вызывается
В данном случае проблема может показаться очевидной и интуитивно понятной. Но интуиция может подвести в других случаях, когда из памяти «всплывают» идеи, навеянные работой с компонентами, основанными на классах.
Например, предположим, мы создаём счётчик, который увеличивается каждую секунду. Если использовать для его реализации класс, то внутреннее чутьё подскажет нам следующее: «Один раз настроить setInterval
для запуска счётчика и один раз использовать clearInterval
для его остановки». Вот пример реализации этого механизма. Когда мы, в голове, переносим подобный подход, планируя воспользоваться useEffect
, то мы, инстинктивно, указываем в качестве зависимостей []
. Запустить-то счётчик нам нужно лишь один раз, верно?
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
Однако, вот незадача, в таком случае счётчик обновится лишь один раз.
Если в голове у вас имеется модель, в соответствии с которой «зависимости позволяют мне указывать на то, когда я хочу повторно вызывать эффект», то этот пример может довести вас до экзистенциального кризиса. Ведь вам нужно, чтобы эффект был вызван лишь один раз, так как в его коде вы, используя setInterval
, запускаете счётчик. Почему же код работает не так, как ожидается?
Но если вы знаете о том, что зависимости — это наша подсказка для React обо всём том, что эффект использует из области видимости рендера, то такое поведение этой программы вас не удивит. А именно, эффект использует count
, но мы не сообщили React правду об этом, указав, в качестве списка зависимостей, пустой массив. И когда эта ложь приведёт к проблемам — лишь вопрос времени.
В первой операции рендеринга count
равняется 0. В результате setCount(count + 1)
в эффекте первого рендера означает setCount(0 + 1)
. Так как мы никогда этот эффект повторно не вызываем, причиной чему — зависимости в виде []
, каждую секунду будет вызываться setCount(0 + 1)
:
// Первый рендеринг, состояние равно 0
function Counter() {
// ...
useEffect(
// Эффект из первого рендера
() => {
const id = setInterval(() => {
setCount(0 + 1); // Всегда setCount(1)
}, 1000);
return () => clearInterval(id);
},
[] // Никогда не перезапускается
);
// ...
}
// В каждом следующем рендере состояние равно 1
function Counter() {
// ...
useEffect(
// Этот эффект всегда игнорируется из-за того, что
// мы солгали React о зависимостях, передав пустой массив.
() => {
const id = setInterval(() => {
setCount(1 + 1);
}, 1000);
return () => clearInterval(id);
},
[]
);
// ...
}
Мы солгали React, сообщив о том, что наш эффект не зависит от значений из компонента, хотя на самом деле — зависит.
Наш эффект использует count
— значение, находящееся внутри компонента (но за пределами эффекта):
const count = // ...
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
В результате указание пустого массива в качестве списка зависимостей приводит к ошибке. React сравнит зависимости и не станет повторно вызывать эффект.
Зависимости не меняются, поэтому вызов эффекта можно пропустить
Непросто искать решения проблем такого рода в уме. Поэтому я советую вам жёстко придерживаться правила, которое заключается в том, что React всегда нужно честно сообщать о зависимостях эффектов, и в том, чтобы указывать все эти зависимости. Если вы хотите получить поддержку линтера в выполнении этого правила — мы приготовили кое-что для вас и для вашей команды.
Два подхода к честности при работе с зависимостями
Для того чтобы всегда честно сообщать React о зависимостях эффектов, можно воспользоваться одной из двух стратегий. Обычно стоит начать с первой, а затем, если это будет нужно, обратиться ко второй.
Первая стратегия заключается в исправлении массива зависимостей, во внесении в него всех значений, находящихся в компоненте, которые используются внутри эффекта. Добавим в массив зависимостей count
:
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
Теперь массив зависимостей исправлен. Возможно, такое решение не идеально, но это — первая проблема, которую нам нужно решить. Теперь изменение count
приведёт к перезапуску эффекта, каждый следующий вызов счётчика будет ссылаться на значение count
из его рендера, выполняя операцию setCount(count + 1)
:
// Первый рендеринг, состояние равно 0
function Counter() {
// ...
useEffect(
// Эффект из первого рендера
() => {
const id = setInterval(() => {
setCount(0 + 1); // setCount(count + 1)
}, 1000);
return () => clearInterval(id);
},
[0] // [count]
);
// ...
}
// Второй рендер, состояние равно 1
function Counter() {
// ...
useEffect(
// Эффект из второго рендера
() => {
const id = setInterval(() => {
setCount(1 + 1); // setCount(count + 1)
}, 1000);
return () => clearInterval(id);
},
[1] // [count]
);
// ...
}
Такой подход позволяет решить проблему, но setInterval
будет, при каждом изменении count
, очищаться и запускаться снова. Вероятно, нас это не устроит.
Зависимости различаются, поэтому эффект мы перезапускаем
Вторая стратегия заключается в изменении кода эффекта таким образом, чтобы ему не понадобилось бы значение, которое меняется чаще, чем нам нужно. Нам не нужно лгать о зависимостях — мы просто хотим изменить эффект так, чтобы у него было бы меньше зависимостей.
Рассмотрим несколько распространённых подходов избавления от зависимостей.
Делаем эффект самодостаточным
Итак, мы хотим избавиться от зависимости count
в эффекте.
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
Для того чтобы это сделать, зададимся вопросом о том, для чего мы используем count
. Возникает такое ощущение, что мы используем count
только в вызове setCount
. В таком случае нам, на самом деле, совершенно не нужно иметь count
в области видимости. Когда мы хотим обновить состояние, основываясь на предыдущем состоянии, мы можем использовать функциональную форму обновления setState
:
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
Я предпочитаю рассматривать подобные случаи как «ненастоящие зависимости». Да, значение count
было необходимой зависимостью из-за того, что мы использовали внутри эффекта конструкцию setCount(count + 1)
. Однако count
нам по-настоящему нужно лишь для того, чтобы преобразовать это значение в count + 1
и «вернуть» его React. Но React уже знает о текущем значении count
. Всё, что нам нужно сообщить React — это сведения о том, что соответствующее значение состояния, в его текущем виде, нужно увеличить на единицу.
Именно эту задачу и решает конструкция setCount(c => c + 1)
. Её можно воспринимать как «отправку React инструкции», описывающей то, как должно изменяться состояние. Такая «форма обновления» оказывается полезной и в других случаях, например, если выполняется объединение множества обновлений.
Обратите внимание на то, что мы, на самом деле, избавились от зависимости. И мы при этом не обманываем React. Наш эффект больше не выполняет чтение значения count
из области видимости рендера:
Зависимости не меняются, поэтому эффект повторно не вызывается
Испытать этот пример можно здесь.
Даже хотя этот эффект вызывается лишь один раз, коллбэк setInterval
, который принадлежит первому рендеру, прекрасно справляется с отправкой инструкции c => c + 1
при каждом срабатывании таймера. Ему не нужно знать текущее значение count
. React уже известно это значение.
Функциональные обновления и Google Docs
Помните, как мы говорили о том, что синхронизация — это основа ментальной модели эффектов? Интересным аспектом синхронизации является тот факт, что часто нужно, чтобы «сообщения», передаваемые между системами, не были бы привязаны к их состоянию. Например, правка документа в Google Docs не приводит к отправке всей страницы на сервер. Это было бы очень неэффективным решением. Вместо этого на сервер отправляется представление того, что попытался сделать пользователь.
Хотя наш случай и отличается от вышеописанного, похожие рассуждения применимы и к эффектам. Подобный подход способствует отправке из эффектов в компонент лишь минимально необходимого объёма информации. Использование функциональной формы системы обновления состояния, выраженной в виде setCount(c => c + 1)
, приводит к передаче гораздо меньшего объёма информации, чем использование конструкции вида setCount(count + 1)
, так как функциональная форма обновления состояния не «загрязнена» текущим значением count
. Она лишь описывает действие, которое нужно выполнить (то есть — «увеличение»). «Думать в стиле React» — значит искать минимально возможное представление состояния. Тот же принцип применим и при планировании обновлений.
Выражение в коде намерения (а не описание в нём результата) похоже на то, как Google Docs решает проблему совместного редактирования документов. Хотя это — и не вполне точная аналогия, функциональные обновления играют в React похожую роль. Они позволяют обеспечить то, что обновления, исходящие из нескольких источников (обработчики событий, подписки эффектов, и так далее) могут быть корректно и предсказуемо применены в пакетном режиме.
Однако даже решение, в котором используется конструкция setCount(c => c + 1)
, нельзя признать безупречным. Выглядит оно немного странно, да и возможности такой конструкции очень ограничены. Например, нам это не поможет в том случае, когда в состоянии имеются две переменные, значения которых зависят друг от друга, или тогда, когда следующий вариант состояния нужно получить на основе свойств. К счастью у setCount(c => c + 1)
есть более мощный родственный паттерн. Он называется useReducer
.
Отделение обновлений от действий
Давайте модифицируем предыдущий пример так, чтобы в состоянии было бы две переменных: count
и step
. В setInterval
счётчик будет увеличиваться на значение, записанное в step
:
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e => setStep(Number(e.target.value))} />
</>
);
}
Вот рабочая версия этого примера.
Обратите внимание на то, что мы тут React не обманываем. Так как теперь в эффекте используется step
, соответствующим образом изменён список зависимостей. И именно поэтому код выполняется правильно.
Сейчас этот пример работает так: изменение step
перезапускает setInterval
— так как step
является одной из зависимостей эффекта. И, во многих случаях, это именно то, что нужно разработчику! Нет ничего плохого в том, чтобы разрушать то, что было создано средствами эффекта и создавать это заново, и мы, если только на то нет веской причины, не должны этого избегать.
Но давайте предположим, что нам нужно, чтобы таймер, создаваемый с помощью setInterval
, не сбрасывался бы при изменении step
. Как убрать зависимость step
из эффекта?
Когда установка значения переменной состояния зависит от текущего значения другой переменной состояния, возможно, имеет смысл заменить обе эти переменных с помощью useReducer
.
Когда вы обнаруживаете, что пишете нечто вроде setSomething(something => ...)
, это значит, что пришло время серьёзно подумать об использовании редьюсера вместо такого кода. Редьюсер позволяет отделять выражения «действий», которые происходят в компоненте, от того, как в ответ на них обновляется состояние.
Поменяем зависимость нашего эффекта step
на зависимость dispatch
:
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' }); // Вместо setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
Тут можно посмотреть этот код в деле.
Вы можете задать мне вопрос: «А чем это лучше того, что было?». Ответ заключается в том, что React гарантирует то, что функция dispatch
будет неизменна в течение времени жизни компонента. Поэтому в вышеприведённом примере даже не нужно выполнять повторное создание таймера.
Мы решили проблему!
(Вы можете опустить значения dispatch
и setstate
и воспользоваться механизмом контейнеризации значений useRef
для работы со значениями из зависимостей, так как React гарантирует то, что они будут статичными. Но если их указать — делу это не повредит.)
Внутри эффекта, вместо считывания значений из состояния, выполняется диспетчеризация действия, которое описывает сведения о том, что произошло. Это позволяет нашему эффекту оставаться отделённым от значения состояния step
. Эффект не заботит то, как именно мы обновляем состояние. Он просто сообщает нам о том, что произошло. А логика обновления собрана в редьюсере:
const initialState = {
count: 0,
step: 1,
};
function reducer(state, action) {
const { count, step } = state;
if (action.type === 'tick') {
return { count: count + step, step };
} else if (action.type === 'step') {
return { count, step: action.step };
} else {
throw new Error();
}
}
Вот, на тот случай, если вы не видели его раньше, полный код этого примера.
Использование useReducer — это чит-режим хуков
Мы узнали о том, как избавляться от зависимостей в том случае, когда эффекту нужно устанавливать значение переменной состояния, основываясь на предыдущей версии состояния или на другой переменной состояния. Но что если нам, для нахождения следующей версии состояния, нужны свойства? Например, возможно, наше API имеет вид <Counter step={1} />
. Очевидно, в такой ситуации нельзя избежать указания props.step
в качестве зависимости эффекта?
На самом деле, избавиться от зависимостей можно и в этом случае! Редьюсер можно поместить в компонент, что позволит ему считывать значения свойств:
function Counter({ step }) {
const [count, dispatch] = useReducer(reducer, 0);
function reducer(state, action) {
if (action.type === 'tick') {
return state + step;
} else {
throw new Error();
}
}
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' });
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
return <h1>{count}</h1>;
}
Применение этого паттерна мешает некоторым оптимизациям, поэтому постарайтесь не использовать его повсюду. Однако, если это нужно, то вы, прибегая к нему, сможете получить доступ к свойствам из редьюсера. Вот работающий пример.
Даже в этом случае гарантируется стабильность сущности dispatch
в разных операциях рендеринга. Поэтому её, если нужно, можно убрать из зависимостей эффекта. Её использование не приведёт к перезапуску эффекта.
Возможно, сейчас вы задаётесь вопросом о том, что позволяет всему этому правильно работать. Откуда редьюсер «знает» значения свойств, когда вызывается внутри эффекта, который принадлежит другому рендеру? Ответ заключается в том, что когда выполняется функция dispatch
, React просто запоминает действие. Он вызовет редьюсер в ходе следующей операции рендеринга. В этот момент в области видимости будут свежие свойства и вы не будете находиться внутри эффекта.
Именно поэтому я предпочитаю воспринимать использование useReducer
как нечто вроде «чит-режима» хуков. Это позволяет мне отделять логику обновления от описания того, что произошло. Это, в свою очередь, помогает мне избавляться от ненужных зависимостей эффектов и избегать их перезапуска, выполняемого чаще, чем необходимо.
Перемещение функций в эффекты
Обычная ошибка при работе с эффектами заключается в том, что функции считают чем-то таким, что не должно присутствовать в составе зависимостей эффектов.
Например, следующий код, вроде бы, выглядит рабочим:
function SearchResults() {
const [data, setData] = useState({ hits: [] });
async function fetchData() {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=react',
);
setData(result.data);
}
useEffect(() => {
fetchData();
}, []); // Нормально ли это?
// ...
Этот пример подготовлен на основе данной отличной статьи, на которую я рекомендую вам взглянуть.
На самом деле, надо отметить, что этот код всё же работает. Но проблема, выражающаяся в том, что локальную функцию не включили в состав зависимостей эффекта, заключается в том, что, по мере роста компонента, становится сложно понять то, вызывается ли эффект во всех тех случаях, когда он должен вызываться.
Представим, что код нашей функции оказывается разделённым так, как показано ниже, и кроме того то, что он стал в пять раз больше:
function SearchResults() {
// Представим, что эта функция имеет большой размер
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=react';
}
// Представим, что и код этой функции гораздо длиннее
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}
useEffect(() => {
fetchData();
}, []);
// ...
}
Теперь предположим, что, в ходе работы над компонентом мы решили использовать в одной из этих функций свойства или состояние:
function SearchResults() {
const [query, setQuery] = useState('react');
// Представим, что эта функция имеет большой размер
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
// Представим, что и код этой функции гораздо длиннее
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}
useEffect(() => {
fetchData();
}, []);
// ...
}
Если мы забудем обновить зависимости любого эффекта, вызывающего эти функции (возможно, через обращение к другим функциям), то эффект не сможет синхронизировать изменения свойств и состояния. Звучит это не особенно приятно.
К счастью, у этой проблемы есть простое решение. Если некоторые функции используются только внутри некоего эффекта, их объявления нужно переместить прямо внутрь этого эффекта:
function SearchResults() {
// ...
useEffect(() => {
// Мы переместили эти функции внутрь эффекта!
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=react';
}
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}
fetchData();
}, []); // С зависимостями всё хорошо.
// ...
}
Вот рабочий вариант этого примера.
В чём же заключаются сильные стороны перемещения функций в эффекты? Дело в том, что при таком подходе нам больше не приходится думать о «промежуточных зависимостях». Наш массив зависимостей больше не лжёт React, так как мы по-настоящему не используем в эффекте ничего из внешней области видимости компонента.
Если позже мы отредактируем код getFetchUrl
, решив воспользоваться там переменной состояния query
, то мы, вероятнее всего, заметим, что редактируем код внутри эффекта. А значит — мы поймём, что query
надо добавить в зависимости эффекта:
function SearchResults() {
const [query, setQuery] = useState('react');
useEffect(() => {
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}
fetchData();
}, [query]); // С зависимостями всё хорошо.
// ...
}
Вот демонстрационная версия этого примера.
Добавляя эту зависимость, мы не просто «успокаиваем React». Её наличие позволяет перезагрузить данные при изменении query
. То, как устроены эффекты, принуждает программиста к тому, чтобы он замечал бы изменения в потоке данных и указывал бы на то, как эффекты должны их синхронизировать. Это куда лучше, чем закрывать глаза на такие изменения до тех пор, пока подобное не вызовет ошибку.
Благодаря правилу линтера exhaustive-deps
из плагина eslint-plugin-react-hooks
можно анализировать код эффектов в процессе его ввода и видеть подсказки, касающиеся неуказанных зависимостей. Другими словами, компьютер может сообщить программисту о том, какие изменения в потоке данных не обрабатываются компонентом правильно.
Линтер в действии
Это очень удобно.
Как быть, если поместить функцию внутрь эффекта нельзя?
Иногда перемещение функции внутрь эффекта может оказаться невозможным. Например, несколько эффектов в одном и том же компоненте могут вызывать одну и ту же функцию, и программист не хочет создавать несколько копий такой функции. Или, возможно, эта функция хранится в свойствах компонента.
Можно ли не указывать подобные функции в составе зависимостей эффекта? Я думаю, что нельзя. Повторюсь: эффект не должен лгать React о зависимостях. Обычно можно найти достойное решение подобной проблемы. Типичным заблуждением в подобной ситуации является мысль о том, что «функция никогда не изменится». Но, как мы уже видели, это совсем не так. На самом деле, функция, объявленная внутри компонента, изменяется в каждой операции рендеринга!
Это, и само по себе, является проблемой. Предположим, что два эффекта вызывают функцию getFetchUrl
:
function SearchResults() {
function getFetchUrl(query) {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
useEffect(() => {
const url = getFetchUrl('react');
// ... Загрузим данные и что-то с ними сделаем...
}, []); // Отсутствующая зависимость: getFetchUrl
useEffect(() => {
const url = getFetchUrl('redux');
// ... Загрузим данные и что-то с ними сделаем...
}, []); // Отсутствующая зависимость: getFetchUrl
// ...
}
В подобной ситуации перемещение функции getFetchUrl
в один из эффектов — не лучшая идея, так как это не позволит организовать её совместное использование несколькими эффектами.
С другой стороны, если быть «честным» при указании зависимостей, можно столкнуться с проблемой. Так как оба эффекта зависят от функции getFetchUrl
(которая, в разных операциях рендеринга, представлена разными сущностями), массивы зависимостей оказываются бесполезными:
function SearchResults() {
// Повторно вызывается в каждой операции рендеринга
function getFetchUrl(query) {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
useEffect(() => {
const url = getFetchUrl('react');
// ... Загрузим данные и что-то с ними сделаем ...
}, [getFetchUrl]); // Зависимости настроены правильно, но они изменяются слишком часто.
useEffect(() => {
const url = getFetchUrl('redux');
// ... Загрузим данные и что-то с ними сделаем...
}, [getFetchUrl]); // Зависимости настроены правильно, но они изменяются слишком часто.
// ...
}
Эту проблему так и хочется решить, просто исключив функцию getFetchUrl
из списка зависимостей. Но я не думаю, что это — хорошее решение. Из-за этого сложнее будет ухватить тот момент, когда мы вносим в поток данных изменения, которые должны быть обработаны эффектом. Это ведёт к ошибкам наподобие той, связанной с неправильно работающим таймером, никогда не обновляющим данные, которую мы уже видели.
Вместо этого — вот два более простых варианта решения данной проблемы.
Для начала, если функция не использует ничего из области видимости компонента, её можно вынести за пределы компонента и спокойно использовать в эффектах:
// Поток данных на эту функцию не влияет
function getFetchUrl(query) {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
function SearchResults() {
useEffect(() => {
const url = getFetchUrl('react');
// ... Загрузим данные и что-то с ними сделаем...
}, []); // С зависимостями всё в порядке.
useEffect(() => {
const url = getFetchUrl('redux');
// ... Загрузим данные и что-то с ними сделаем...
}, []); // С зависимостями всё в порядке.
// ...
}
Эту функцию не нужно указывать в составе зависимостей, так как она не находится в области видимости рендера и на неё не может подействовать поток данных. И она не может, по случайности, стать зависимой от свойств или состояния.
Вот ещё один вариант решения этой проблемы. Он заключается в том, что функцию можно обернуть в хук useCallback:
function SearchResults() {
// Если зависимости не меняются, сущность сохраняется
const getFetchUrl = useCallback((query) => {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, []); // С зависимостями коллбэка всё в порядке.
useEffect(() => {
const url = getFetchUrl('react');
// ... Загрузим данные и что-то с ними сделаем...
}, [getFetchUrl]); // С зависимостями эффекта всё в порядке.
useEffect(() => {
const url = getFetchUrl('redux');
// ... Загрузим данные и что-то с ними сделаем...
}, [getFetchUrl]); // С зависимостями эффекта всё в порядке
// ...
}
Использование useCallback
напоминает добавление в систему ещё одного уровня проверки зависимостей. Использование этого механизма представляет собой подход к решению нашей проблемы с другой стороны: вместо того, чтобы избегать функций-зависимостей, мы делаем так, что сама функция меняется только тогда, когда это необходимо.
Рассмотрим этот подход и поговорим о том, почему его применение целесообразно. Ранее наш пример выводил результаты поиска по двум запросам ('react'
и 'redux'
). Но предположим, что мы хотим добавить в компонент поле ввода, которое позволяет пользователю приложения выполнять поиск по любому запросу, представленному свойством состояния query
. В результате, вместо того, чтобы рассматривать query
в виде аргумента функции, getFetchUrl
теперь читает соответствующее значение из локального состояния.
Попытавшись сделать это, мы тут же заметим отсутствие зависимости query
в useCallback
:
function SearchResults() {
const [query, setQuery] = useState('react');
const getFetchUrl = useCallback(() => { // Нет аргумента query
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, []); // Отсутствующая зависимость: query
// ...
}
Если исправить зависимости useCallback
и включить в их состав query
, то любой эффект, в зависимостях которого есть getFetchUrl
, будет перезапускаться при каждом изменении query
:
function SearchResults() {
const [query, setQuery] = useState('react');
// Сущность не меняется до изменения query
const getFetchUrl = useCallback(() => {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, [query]); // Зависимости коллбэка в порядке.
useEffect(() => {
const url = getFetchUrl();
// ... Загрузим данные и что-то с ними сделаем...
}, [getFetchUrl]); // Зависимости эффекта в порядке.
// ...
}
Благодаря использованию useCallback
, если query
не меняется, то и getFetchUrl
не меняется, а значит, не происходит и перезапуска эффекта. Но если query
меняется, тогда изменится и getFetchUrl
, и мы выполним повторную загрузку данных. Это похоже на работу в Excel: если изменить значение в какой-то ячейке, то значения в других ячейках, зависящие от значения изменённой ячейки, будут автоматически пересчитаны.
Это — всего лишь последствия того, что мы принимаем во внимание поток данных и рассматриваем систему с точки зрения синхронизации. То же самое решение работает и для свойств функций, переданных от родительских сущностей:
function Parent() {
const [query, setQuery] = useState('react');
// Сущность не меняется до изменения query
const fetchData = useCallback(() => {
const url = 'https://hn.algolia.com/api/v1/search?query=' + query;
// ... Загрузим данные и вернём их ...
}, [query]); // С зависимостями коллбэка всё в порядке
return <Child fetchData={fetchData} />
}
function Child({ fetchData }) {
let [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, [fetchData]); // С зависимостями эффекта всё в порядке
// ...
}
Так как fetchData
из Parent
изменяется лишь при изменении значения состояния query
, Child
не будет выполнять перезагрузку данных до тех пор, пока это не будет нужно приложению.
Являются ли функции частью потока данных?
Интересно то, что этот паттерн не работает при его использовании с компонентами, основанными на классах, причём, причины этого хорошо иллюстрируют разницу между парадигмами эффектов и методов жизненного цикла компонентов. Рассмотрим следующий код, представляющий собой систему компонентов, основанных на классах, в котором сделана попытка реализовать те же возможности, что и в предыдущем примере:
class Parent extends Component {
state = {
query: 'react'
};
fetchData = () => {
const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;
// ... Загрузим данные и что-то с ними сделаем...
};
render() {
return <Child fetchData={this.fetchData} />;
}
}
class Child extends Component {
state = {
data: null
};
componentDidMount() {
this.props.fetchData();
}
render() {
// ...
}
}
Возможно, вы думаете сейчас: «Да ладно, Дэн, все мы знаем, что useEffect
— это нечто вроде комбинации componentDidMount
и componentDidUpdate
. Хватит уже об этом говорить!». Однако работать это не будет даже при использовании componentDidUpdate
:
class Child extends Component {
state = {
data: null
};
componentDidMount() {
this.props.fetchData();
}
componentDidUpdate(prevProps) {
// Это условие никогда не будет истинным
if (this.props.fetchData !== prevProps.fetchData) {
this.props.fetchData();
}
}
render() {
// ...
}
}
Конечно, fetchData
— это метод класса! (Или, скорее, свойство класса, но это ничего не меняет.) Этот метод не изменится только из-за того, что изменилось состояние. Поэтому this.props.fetchData
будет оставаться равным prevProps.fetchData
и повторная загрузка данных никогда выполнена не будет. Тогда, может быть, уберём условие?
componentDidUpdate(prevProps) {
this.props.fetchData();
}
Но здесь тоже не всё благополучно. Теперь загрузка данных будет выполняться при каждом повторном рендеринге компонента. (Интересным способом подтвердить это будет добавление анимации.) Может быть, надо привязать fetchData
к значению this.state.query
?
render() {
return <Child fetchData={this.fetchData.bind(this, this.state.query)} />;
}
Но тогда условие this.props.fetchData !== prevProps.fetchData
всегда будет давать true
, даже в том случае, если query
не меняется! В результате мы постоянно будем выполнять повторную загрузку данных.
Единственное реальное решение этой головоломки компонентов, основанных на классах, заключается в том, чтобы проявить мужество и передать само значение query
компоненту Child
. Этот компонент, сам по себе, не будет использовать query
, но это может вызвать повторную загрузку данных при изменении query
:
class Parent extends Component {
state = {
query: 'react'
};
fetchData = () => {
const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;
// ... Загрузим данные и что-то с ними сделаем ...
};
render() {
return <Child fetchData={this.fetchData} query={this.state.query} />;
}
}
class Child extends Component {
state = {
data: null
};
componentDidMount() {
this.props.fetchData();
}
componentDidUpdate(prevProps) {
if (this.props.query !== prevProps.query) {
this.props.fetchData();
}
}
render() {
// ...
}
}
За годы работы с компонентами, основанными на классах, я так привык передавать компонентам-потомкам ненужные свойства и нарушать инкапсуляцию родительских компонентов, что лишь недавно понял то, почему мы вынуждены так поступать.
При работе с классами функциональные свойства, сами по себе, не являются настоящей частью потока данных. Методы используют мутабельную сущность this
, поэтому нельзя полагаться на выяснение идентичности этих методов. Таким образом, если нам нужно работать с функцией, нам приходится манипулировать другими данными для того, чтобы можно было бы понять, изменилось что-то или нет. Мы не можем выяснить, зависит ли функция this.props.fetchData
, переданная из родительского компонента дочернему, от неких данных состояния, или нет, и о том, изменились ли эти данные.
Функции могут по-настоящему включаться в поток данных благодаря использованию useCallback
. Мы можем сказать, что, если входные данные функции изменились, то и сама функция изменилась. Если же этого не произошло, то неизменной осталась и функция. Благодаря особенностям useCallback
изменения свойств наподобие props.fetchData
могут распространяться автоматически.
Аналогично, useMemo
позволяет делать то же самое со сложными объектами:
function ColorPicker() {
// Не нарушает неглубокую проверку на равенство свойств компонента Child,
// система реагирует лишь на реальное изменение цвета.
const [color, setColor] = useState('pink');
const style = useMemo(() => ({ color }), [color]);
return <Child style={style} />;
}
Мне хотелось бы подчеркнуть то, что если всюду использовать useCallback
, это сделает код довольно-таки громоздким. Этот механизм представляет собой хороший «запасной выход», он полезен в тех случаях, когда функция и передаётся дочерним компонентам, и вызывается внутри их эффектов. Или в тех случаях, когда нужно предотвратить нарушение мемоизации дочернего компонента. Но хуки лучше отражают модель системы, в которой полностью избегают передачи коллбэков дочерним компонентам.
В вышеприведённых примерах я предпочитаю, чтобы функция fetchData
присутствовала бы либо внутри эффекта (который можно преобразовать в собственный хук), либо была бы представлена импортированной извне сущностью. Я стремлюсь к тому, чтобы эффекты были бы простыми, и коллбэки в них этому не способствуют. («Что если коллбэк props.onComplete
изменится в то время, пока на отправленный запрос не получено ответа?») Можно имитировать поведение класса, но это не решит проблему состояния гонки.
Состояние гонки
Вот как может выглядеть традиционный пример загрузки данных в компоненте, основанном на классе:
class Article extends Component {
state = {
article: null
};
componentDidMount() {
this.fetchData(this.props.id);
}
async fetchData(id) {
const article = await API.fetchArticle(id);
this.setState({ article });
}
// ...
}
Как вы, возможно, знаете, этот код содержит ошибки. Он не поддерживает обновления. А вот — ещё один подобный пример, который можно найти в интернете:
class Article extends Component {
state = {
article: null
};
componentDidMount() {
this.fetchData(this.props.id);
}
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.fetchData(this.props.id);
}
}
async fetchData(id) {
const article = await API.fetchArticle(id);
this.setState({ article });
}
// ...
}
Этот код, определённо, лучше, но в нём всё ещё есть проблемы. Причина этого заключается в том, что запросы могут идти не по порядку. Например, я загружаю статью с {id: 10}
, потом перехожу на статью с {id: 20}
, выполняя ещё один запрос, и ответ на этот запрос приходит до прихода ответа на первый запрос. В результате запрос, который начался раньше, но ответ на который пришёл позже, перезапишет состояние. А это неправильно.
То, о чём мы тут говорим, называется состоянием гонки. Это — ситуация, типичная для кода, в котором конструкция async/await
(применение которой означает, что нечто ожидает какого-то результата) смешивается с потоком данных, направленным сверху вниз (свойства и состояние не могут изменяться в то время, когда мы находимся в асинхронной функции).
Эффекты не дают нам некоего чудесного решения этой проблемы, хотя программист и получит предупреждение при попытке непосредственной передачи эффекту async
-функции. (Нам, кстати, надо улучшить это предупреждение так, чтобы оно лучше описывало проблемы, которые это может вызвать.)
Если используемые вами асинхронные механизмы поддерживают отмену операций, то надо отметить, что это замечательно! Это позволяет отменить асинхронный запрос прямо в функции очистки.
Кроме того, простейшим временным решением этой проблемы является контроль асинхронных операций с помощью логических переменных:
function Article({ id }) {
const [article, setArticle] = useState(null);
useEffect(() => {
let didCancel = false;
async function fetchData() {
const article = await API.fetchArticle(id);
if (!didCancel) {
setArticle(article);
}
}
fetchData();
return () => {
didCancel = true;
};
}, [id]);
// ...
}
В этом материале можно найти подробности о том, как обрабатывать ошибки и состояния загрузки, а также о том, как извлекать подобную логику в собственные хуки. Если вам интересна тема загрузки данных с использованием хуков, я рекомендую вам разобраться с вышеупомянутым материалом.
Поднимаем планку
Если рассматривать побочные эффекты с позиций методов жизненного цикла компонентов, основанных на классах, то окажется, что они ведут себя не так, как то, что рендерит компонент. Рендерингом пользовательского интерфейса управляют свойства и состояние, и интерфейс, гарантированно, будет им соответствовать. В случае же с побочными эффектами это не так. Это — распространённый источник ошибок.
Если смотреть на вещи с точки зрения useEffect
, то всё, по умолчанию, является синхронизированным. Побочные эффекты стали частью потока данных React. Если сделать всё правильно, то при каждом вызове useEffect
компонент гораздо лучше обрабатывает пограничные случаи.
Но надо отметить, что для того, чтобы «сделать всё правильно», нужно заранее вложить в проект немало сил и времени. И это может раздражать разработчиков. Хорошо написать код синхронизации, поддерживающий пограничные случаи, по сути, гораздо сложнее, чем вызвать «одноразовый» побочный эффект, который не согласован с результатами рендеринга.
Это может оказаться неудобным в том случае, если useEffect
играет роль инструмента, которым вы пользуетесь постоянно. Однако это — низкоуровневый строительный блок приложений. Сейчас — самое начало внедрения хуков, поэтому все, особенно — в учебных руководствах, постоянно используют низкоуровневые примеры их применения. Но на практике, вероятнее всего, сообщество будет двигаться в сторону высокоуровневых хуков, по мере того, как будут набирать популярность хорошие API.
Я видел, как в различных приложениях создаются их собственные хуки, наподобие useFetch
, который инкапсулирует некоторую логику аутентификации таких приложений, или useTheme
, который использует контекст темы. После того, как вы освоитесь с этими инструментами, вы не особенно часто будете прибегать к использованию useEffect
. Но гибкость, предоставляемая этим механизмом, идёт на пользу каждому хуку, построенному на его основе.
До сих пор, например, useEffect
наиболее часто используется для загрузки данных. Но загрузка данных — это не совсем то, что относится к проблеме синхронизации. Это особенно очевидно по той причине, что зависимости в таких случаях обычно представлены пустым массивом. Что мы вообще синхронизируем с их помощью?
В долгосрочной перспективе применение механизма Suspense для загрузки данных даст сторонним библиотекам отличный способ сообщить React о том, что рендеринг надо приостановить до тех пор, пока что-то асинхронное (что угодно: код, данные, изображения) не будет готово к выводу.
Так как возможности Suspense
постепенно покрывают всё больше сценариев загрузки данных, я ожидаю, что useEffect
постепенно отойдёт на второй план, став инструментом продвинутых программистов, которым пользуются в случаях, когда нужно синхронизировать свойства и состояние с каким-нибудь побочным эффектом. В отличие от того, как этот механизм работает с загрузкой данных, его применение для подобных целей выглядит совершенно естественным, так как он был спроектирован именно для решения задач синхронизации. Но до тех пор собственные хуки, вроде тех, что показаны здесь, будут представлять собой хороший способ многократного использования логики, ответственной за загрузку данных.
Итоги
Теперь вы знаете об эффектах практически всё, что знаю я. И если вы, начиная читать этот материал и просмотрев раздел с ответами на вопросы, столкнулись с чем-то непонятным, теперь, надеюсь, всё встало на свои места.
useEffect
is a React Hook that lets you synchronize a component with an external system.
useEffect(setup, dependencies?)
- Reference
useEffect(setup, dependencies?)
- Usage
- Connecting to an external system
- Wrapping Effects in custom Hooks
- Controlling a non-React widget
- Fetching data with Effects
- Specifying reactive dependencies
- Updating state based on previous state from an Effect
- Removing unnecessary object dependencies
- Removing unnecessary function dependencies
- Reading the latest props and state from an Effect
- Displaying different content on the server and the client
- Troubleshooting
- My Effect runs twice when the component mounts
- My Effect runs after every re-render
- My Effect keeps re-running in an infinite cycle
- My cleanup logic runs even though my component didn’t unmount
- My Effect does something visual, and I see a flicker before it runs
Reference
useEffect(setup, dependencies?)
Call useEffect
at the top level of your component to declare an Effect:
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]);
// ...
}
See more examples below.
Parameters
-
setup
: The function with your Effect’s logic. Your setup function may also optionally return a cleanup function. When your component is first added to the DOM, React will run your setup function. After every re-render with changed dependencies, React will first run the cleanup function (if you provided it) with the old values, and then run your setup function with the new values. After your component is removed from the DOM, React will run your cleanup function one last time. -
optional
dependencies
: The list of all reactive values referenced inside of thesetup
code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. If your linter is configured for React, it will verify that every reactive value is correctly specified as a dependency. The list of dependencies must have a constant number of items and be written inline like[dep1, dep2, dep3]
. React will compare each dependency with its previous value using theObject.is
comparison. If you omit this argument, your Effect will re-run after every re-render of the component. See the difference between passing an array of dependencies, an empty array, and no dependencies at all.
Returns
useEffect
returns undefined
.
Caveats
-
useEffect
is a Hook, so you can only call it at the top level of your component or your own Hooks. You can’t call it inside loops or conditions. If you need that, extract a new component and move the state into it. -
If you’re not trying to synchronize with some external system, you probably don’t need an Effect.
-
When Strict Mode is on, React will run one extra development-only setup+cleanup cycle before the first real setup. This is a stress-test that ensures that your cleanup logic “mirrors” your setup logic and that it stops or undoes whatever the setup is doing. If this causes a problem, implement the cleanup function.
-
If some of your dependencies are objects or functions defined inside the component, there is a risk that they will cause the Effect to re-run more often than needed. To fix this, remove unnecessary object and function dependencies. You can also extract state updates and non-reactive logic outside of your Effect.
-
If your Effect wasn’t caused by an interaction (like a click), React will let the browser paint the updated screen first before running your Effect. If your Effect is doing something visual (for example, positioning a tooltip), and the delay is noticeable (for example, it flickers), replace
useEffect
withuseLayoutEffect
. -
Even if your Effect was caused by an interaction (like a click), the browser may repaint the screen before processing the state updates inside your Effect. Usually, that’s what you want. However, if you must block the browser from repainting the screen, you need to replace
useEffect
withuseLayoutEffect
. -
Effects only run on the client. They don’t run during server rendering.
Usage
Connecting to an external system
Some components need to stay connected to the network, some browser API, or a third-party library, while they are displayed on the page. These systems aren’t controlled by React, so they are called external.
To connect your component to some external system, call useEffect
at the top level of your component:
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]);
// ...
}
You need to pass two arguments to useEffect
:
- A setup function with setup code that connects to that system.
- It should return a cleanup function with cleanup code that disconnects from that system.
- A list of dependencies including every value from your component used inside of those functions.
React calls your setup and cleanup functions whenever it’s necessary, which may happen multiple times:
- Your setup code runs when your component is added to the page (mounts).
- After every re-render of your component where the dependencies have changed:
- First, your cleanup code runs with the old props and state.
- Then, your setup code runs with the new props and state.
- Your cleanup code runs one final time after your component is removed from the page (unmounts).
Let’s illustrate this sequence for the example above.
When the ChatRoom
component above gets added to the page, it will connect to the chat room with the initial serverUrl
and roomId
. If either serverUrl
or roomId
change as a result of a re-render (say, if the user picks a different chat room in a dropdown), your Effect will disconnect from the previous room, and connect to the next one. When the ChatRoom
component is removed from the page, your Effect will disconnect one last time.
To help you find bugs, in development React runs setup and cleanup one extra time before the setup. This is a stress-test that verifies your Effect’s logic is implemented correctly. If this causes visible issues, your cleanup function is missing some logic. The cleanup function should stop or undo whatever the setup function was doing. The rule of thumb is that the user shouldn’t be able to distinguish between the setup being called once (as in production) and a setup → cleanup → setup sequence (as in development). See common solutions.
Try to write every Effect as an independent process and think about a single setup/cleanup cycle at a time. It shouldn’t matter whether your component is mounting, updating, or unmounting. When your cleanup logic correctly “mirrors” the setup logic, your Effect is resilient to running setup and cleanup as often as needed.
Note
Examples of connecting to an external system
Connecting to a chat server
In this example, the ChatRoom
component uses an Effect to stay connected to an external system defined in chat.js
. Press “Open chat” to make the ChatRoom
component appear. This sandbox runs in development mode, so there is an extra connect-and-disconnect cycle, as explained here. Try changing the roomId
and serverUrl
using the dropdown and the input, and see how the Effect re-connects to the chat. Press “Close chat” to see the Effect disconnect one last time.
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> Server URL:{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Welcome to the {roomId} room!</h1> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); const [show, setShow] = useState(false); 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> <button onClick={() => setShow(!show)}> {show ? 'Close chat' : 'Open chat'} </button> {show && <hr />} {show && <ChatRoom roomId={roomId} />} </> ); }
Wrapping Effects in custom Hooks
Effects are an “escape hatch”: you use them when you need to “step outside React” and when there is no better built-in solution for your use case. If you find yourself often needing to manually write Effects, it’s usually a sign that you need to extract some custom Hooks for common behaviors your components rely on.
For example, this useChatRoom
custom Hook “hides” the logic of your Effect behind a more declarative API:
function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
Then you can use it from any component like this:
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...
There are also many excellent custom Hooks for every purpose available in the React ecosystem.
Learn more about wrapping Effects in custom Hooks.
Examples of wrapping Effects in custom Hooks
Custom useChatRoom
Hook
This example is identical to one of the earlier examples, but the logic is extracted to a custom Hook.
import { useState } from 'react'; import { useChatRoom } from './useChatRoom.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); useChatRoom({ roomId: roomId, serverUrl: serverUrl }); return ( <> <label> Server URL:{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Welcome to the {roomId} room!</h1> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); const [show, setShow] = useState(false); 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> <button onClick={() => setShow(!show)}> {show ? 'Close chat' : 'Open chat'} </button> {show && <hr />} {show && <ChatRoom roomId={roomId} />} </> ); }
Controlling a non-React widget
Sometimes, you want to keep an external system synchronized to some prop or state of your component.
For example, if you have a third-party map widget or a video player component written without React, you can use an Effect to call methods on it that make its state match the current state of your React component. This Effect creates an instance of a MapWidget
class defined in map-widget.js
. When you change the zoomLevel
prop of the Map
component, the Effect calls the setZoom()
on the class instance to keep it synchronized:
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} /> ); }
In this example, a cleanup function is not needed because the MapWidget
class manages only the DOM node that was passed to it. After the Map
React component is removed from the tree, both the DOM node and the MapWidget
class instance will be automatically garbage-collected by the browser JavaScript engine.
Fetching data with Effects
You can use an Effect to fetch data for your component. Note that if you use a framework, using your framework’s data fetching mechanism will be a lot more efficient than writing Effects manually.
If you want to fetch data from an Effect manually, your code might look like this:
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]);
// ...
Note the ignore
variable which is initialized to false
, and is set to true
during cleanup. This ensures your code doesn’t suffer from “race conditions”: network responses may arrive in a different order than you sent them.
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> </> ); }
You can also rewrite using the async
/ await
syntax, but you still need to provide a cleanup function:
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> </> ); }
Writing data fetching directly in Effects gets repetitive and makes it difficult to add optimizations like caching and server rendering later. It’s easier to use a custom Hook—either your own or maintained by the community.
Deep Dive
What are good alternatives to data fetching in Effects?
Writing fetch
calls inside Effects is a popular way to fetch data, especially in fully client-side apps. This is, however, a very manual approach and it has significant downsides:
- Effects don’t run on the server. This means that the initial server-rendered HTML will only include a loading state with no data. The client computer will have to download all JavaScript and render your app only to discover that now it needs to load the data. This is not very efficient.
- Fetching directly in Effects makes it easy to create “network waterfalls”. You render the parent component, it fetches some data, renders the child components, and then they start fetching their data. If the network is not very fast, this is significantly slower than fetching all data in parallel.
- Fetching directly in Effects usually means you don’t preload or cache data. For example, if the component unmounts and then mounts again, it would have to fetch the data again.
- It’s not very ergonomic. There’s quite a bit of boilerplate code involved when writing
fetch
calls in a way that doesn’t suffer from bugs like race conditions.
This list of downsides is not specific to React. It applies to fetching data on mount with any library. Like with routing, data fetching is not trivial to do well, so we recommend the following approaches:
- If you use a framework, use its built-in data fetching mechanism. Modern React frameworks have integrated data fetching mechanisms that are efficient and don’t suffer from the above pitfalls.
- Otherwise, consider using or building a client-side cache. Popular open source solutions include React Query, useSWR, and React Router 6.4+. You can build your own solution too, in which case you would use Effects under the hood but also add logic for deduplicating requests, caching responses, and avoiding network waterfalls (by preloading data or hoisting data requirements to routes).
You can continue fetching data directly in Effects if neither of these approaches suit you.
Specifying reactive dependencies
Notice that you can’t “choose” the dependencies of your Effect. Every reactive value used by your Effect’s code must be declared as a dependency. Your Effect’s dependency list is determined by the surrounding code:
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
// ...
}
If either serverUrl
or roomId
change, your Effect will reconnect to the chat using the new values.
Reactive values include props and all variables and functions declared directly inside of your component. Since roomId
and serverUrl
are reactive values, you can’t remove them from the dependencies. If you try to omit them and your linter is correctly configured for React, the linter will flag this as a mistake you need to fix:
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'
// ...
}
To remove a dependency, you need to “prove” to the linter doesn’t need to be a dependency. For example, you can move serverUrl
out of your component to prove that it’s not reactive and won’t change on re-renders:
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
// ...
}
Now that serverUrl
is not a reactive value (and can’t change on a re-render), it doesn’t need to be a dependency. If your Effect’s code doesn’t use any reactive values, its dependency list should be empty ([]
):
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
// ...
}
An Effect with empty dependencies doesn’t re-run when any of your component’s props or state change.
Pitfall
If you have an existing codebase, you might have some Effects that suppress the linter like this:
useEffect(() => {
// ...
// 🔴 Avoid suppressing the linter like this:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);
When dependencies don’t match the code, there is a high risk of introducing bugs. By suppressing the linter, you “lie” to React about the values your Effect depends on. Instead, prove they’re unnecessary.
Examples of passing reactive dependencies
Passing a dependency array
If you specify the dependencies, your Effect runs after the initial render and after re-renders with changed dependencies.
useEffect(() => {
// ...
}, [a, b]); // Runs again if a or b are different
In the below example, serverUrl
and roomId
are reactive values, so they both must be specified as dependencies. As a result, selecting a different room in the dropdown or editing the server URL input causes the chat to re-connect. However, since message
isn’t used in the Effect (and so it isn’t a dependency), editing the message doesn’t re-connect to the chat.
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); const [message, setMessage] = useState(''); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => { connection.disconnect(); }; }, [serverUrl, roomId]); return ( <> <label> Server URL:{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Welcome to the {roomId} room!</h1> <label> Your message:{' '} <input value={message} onChange={e => setMessage(e.target.value)} /> </label> </> ); } export default function App() { const [show, setShow] = useState(false); 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> <button onClick={() => setShow(!show)}> {show ? 'Close chat' : 'Open chat'} </button> </label> {show && <hr />} {show && <ChatRoom roomId={roomId}/>} </> ); }
Updating state based on previous state from an Effect
When you want to update state based on previous state from an Effect, you might run into a problem:
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.
// ...
}
Since count
is a reactive value, it must be specified in the list of dependencies. However, that causes the Effect to cleanup and setup again every time the count
changes. This is not ideal.
To fix this, pass the c => c + 1
state updater to setCount
:
Now that you’re passing c => c + 1
instead of count + 1
, your Effect no longer needs to depend on count
. As a result of this fix, it won’t need to cleanup and setup the interval again every time the count
changes.
Removing unnecessary object dependencies
If your Effect depends on an object or a function created during rendering, it might run too often. For example, this Effect re-connects after every render because the options
object is different for every render:
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
// ...
Avoid using an object created during rendering as a dependency. Instead, create the object inside the Effect:
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} /> </> ); }
Now that you create the options
object inside the Effect, the Effect itself only depends on the roomId
string.
With this fix, typing into the input doesn’t reconnect the chat. Unlike an object which gets re-created, a string like roomId
doesn’t change unless you set it to another value. Read more about removing dependencies.
Removing unnecessary function dependencies
If your Effect depends on an object or a function created during rendering, it might run too often. For example, this Effect re-connects after every render because the createOptions
function is different for every render:
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
// ...
By itself, creating a function from scratch on every re-render is not a problem. You don’t need to optimize that. However, if you use it as a dependency of your Effect, it will cause your Effect to re-run after every re-render.
Avoid using a function created during rendering as a dependency. Instead, declare it inside the Effect:
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} /> </> ); }
Now that you define the createOptions
function inside the Effect, the Effect itself only depends on the roomId
string. With this fix, typing into the input doesn’t reconnect the chat. Unlike a function which gets re-created, a string like roomId
doesn’t change unless you set it to another value. Read more about removing dependencies.
Reading the latest props and state from an Effect
Under Construction
This section describes an experimental API that has not yet been released in a stable version of React.
By default, when you read a reactive value from an Effect, you have to add it as a dependency. This ensures that your Effect “reacts” to every change of that value. For most dependencies, that’s the behavior you want.
However, sometimes you’ll want to read the latest props and state from an Effect without “reacting” to them. For example, imagine you want to log the number of the items in the shopping cart for every page visit:
function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ All dependencies declared
// ...
}
What if you want to log a new page visit after every url
change, but not if only the shoppingCart
changes? You can’t exclude shoppingCart
from dependencies without breaking the reactivity rules. However, you can express that you don’t want a piece of code to “react” to changes even though it is called from inside an Effect. Declare an Effect Event with the useEffectEvent
Hook, and move the code reading shoppingCart
inside of it:
function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}
Effect Events are not reactive and must always be omitted from dependencies of your Effect. This is what lets you put non-reactive code (where you can read the latest value of some props and state) inside of them. By reading shoppingCart
inside of onVisit
, you ensure that shoppingCart
won’t re-run your Effect.
Read more about how Effect Events let you separate reactive and non-reactive code.
Displaying different content on the server and the client
If your app uses server rendering (either directly or via a framework), your component will render in two different environments. On the server, it will render to produce the initial HTML. On the client, React will run the rendering code again so that it can attach your event handlers to that HTML. This is why, for hydration to work, your initial render output must be identical on the client and the server.
In rare cases, you might need to display different content on the client. For example, if your app reads some data from localStorage
, it can’t possibly do that on the server. Here is how you could implement this:
function MyComponent() {
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
}, []);
if (didMount) {
// ... return client-only JSX ...
} else {
// ... return initial JSX ...
}
}
While the app is loading, the user will see the initial render output. Then, when it’s loaded and hydrated, your Effect will run and set didMount
to true
, triggering a re-render. This will switch to the client-only render output. Effects don’t run on the server, so this is why didMount
was false
during the initial server render.
Use this pattern sparingly. Keep in mind that users with a slow connection will see the initial content for quite a bit of time—potentially, many seconds—so you don’t want to make jarring changes to your component’s appearance. In many cases, you can avoid the need for this by conditionally showing different things with CSS.
Troubleshooting
My Effect runs twice when the component mounts
When Strict Mode is on, in development, React runs setup and cleanup one extra time before the actual setup.
This is a stress-test that verifies your Effect’s logic is implemented correctly. If this causes visible issues, your cleanup function is missing some logic. The cleanup function should stop or undo whatever the setup function was doing. The rule of thumb is that the user shouldn’t be able to distinguish between the setup being called once (as in production) and a setup → cleanup → setup sequence (as in development).
Read more about how this helps find bugs and how to fix your logic.
My Effect runs after every re-render
First, check that you haven’t forgotten to specify the dependency array:
useEffect(() => {
// ...
}); // 🚩 No dependency array: re-runs after every render!
If you’ve specified the dependency array but your Effect still re-runs in a loop, it’s because one of your dependencies is different on every re-render.
You can debug this problem by manually logging your dependencies to the console:
useEffect(() => {
// ..
}, [serverUrl, roomId]);
console.log([serverUrl, roomId]);
You can then right-click on the arrays from different re-renders in the console and select “Store as a global variable” for both of them. Assuming the first one got saved as temp1
and the second one got saved as temp2
, you can then use the browser console to check whether each dependency in both arrays is the same:
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 ...
When you find the dependency that is different on every re-render, you can usually fix it in one of these ways:
- Updating state based on previous state from an Effect
- Removing unnecessary object dependencies
- Removing unnecessary function dependencies
- Reading the latest props and state from an Effect
As a last resort (if these methods didn’t help), wrap its creation with useMemo
or useCallback
(for functions).
My Effect keeps re-running in an infinite cycle
If your Effect runs in an infinite cycle, these two things must be true:
- Your Effect is updating some state.
- That state leads to a re-render, which causes the Effect’s dependencies to change.
Before you start fixing the problem, ask yourself whether your Effect is connecting to some external system (like DOM, network, a third-party widget, and so on). Why does your Effect need to set state? Does it synchronize with that external system? Or are you trying to manage your application’s data flow with it?
If there is no external system, consider whether removing the Effect altogether would simplify your logic.
If you’re genuinely synchronizing with some external system, think about why and under what conditions your Effect should update the state. Has something changed that affects your component’s visual output? If you need to keep track of some data that isn’t used by rendering, a ref (which doesn’t trigger re-renders) might be more appropriate. Verify your Effect doesn’t update the state (and trigger re-renders) more than needed.
Finally, if your Effect is updating the state at the right time, but there is still a loop, it’s because that state update leads to one of the Effect’s dependencies changing. Read how to debug dependency changes.
My cleanup logic runs even though my component didn’t unmount
The cleanup function runs not only during unmount, but before every re-render with changed dependencies. Additionally, in development, React runs setup+cleanup one extra time immediately after component mounts.
If you have cleanup code without corresponding setup code, it’s usually a code smell:
useEffect(() => {
// 🔴 Avoid: Cleanup logic without corresponding setup logic
return () => {
doSomething();
};
}, []);
Your cleanup logic should be “symmetrical” to the setup logic, and should stop or undo whatever setup did:
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
Learn how the Effect lifecycle is different from the component’s lifecycle.
My Effect does something visual, and I see a flicker before it runs
If your Effect must block the browser from painting the screen, replace useEffect
with useLayoutEffect
. Note that this shouldn’t be needed for the vast majority of Effects. You’ll only need this if it’s crucial to run your Effect before the browser paint: for example, to measure and position a tooltip before the user sees it.
Использование хука эффекта¶
Хуки — нововведение в React 16.8, которое позволяет использовать состояние и другие возможности React без написания классов.
Хук эффекта даёт вам возможность выполнять побочные эффекты в функциональном компоненте:
import React, { useState, useEffect } from 'react'
function Example() {
const [count, setCount] = useState(0)
// Аналогично componentDidMount и componentDidUpdate:
useEffect(() => {
// Обновляем заголовок документа с помощью API браузера
document.title = `Вы нажали ${count} раз`
})
return (
<div>
<p>Вы нажали {count} раз</p>
<button onClick={() => setCount(count + 1)}>
Нажми на меня
</button>
</div>
)
}
Этот фрагмент основан на примере со счётчиком из предыдущей страницы, только мы добавили новый функционал: мы изменяем заголовок документа на пользовательское сообщение, которое также содержит количество нажатий кнопки.
Побочными эффектами в React-компонентах могут быть: загрузка данных, оформление подписки и изменение DOM вручную. Неважно, называете ли вы эти операции «побочными эффектам» (или просто «эффектами») или нет, скорее всего вам доводилось ранее использовать их в своих компонентах.
Совет
Если вам знакомы классовые методы жизненного цикла React, хук
useEffect
представляет собой совокупность методовcomponentDidMount
,componentDidUpdate
, иcomponentWillUnmount
.
Существует два распространённых вида побочных эффектов в компонентах React: компоненты, которые требуют и не требуют сброса. Давайте рассмотрим оба примера более детально.
Эффекты без сброса¶
Иногда мы хотим выполнить дополнительный код после того, как React обновил DOM. Сетевые запросы, изменения DOM вручную, логирование — всё это примеры эффектов, которые не требуют сброса. После того, как мы запустили их, можно сразу забыть о них, ведь больше никаких дополнительных действий не требуется. Давайте сравним, как классы и хуки позволяют нам реализовывать побочные эффекты.
Пример с использованием классов¶
В классовых React-компонентах метод render
сам по себе не должен вызывать никаких побочных эффектов. Он не подходит для этих целей, так как в основном мы хотим выполнить наши эффекты после того, как React обновил DOM.
Вот почему в классах React мы размещаем побочные эффекты внутрь componentDidMount
и componentDidUpdate
. Возвращаясь к нашему примеру, здесь представлен счётчик, реализованный с помощью классового React-компонента. Он обновляет заголовок документа сразу же после того, как React вносит изменения в DOM:
class Example extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0,
}
}
componentDidMount() {
document.title = `Вы нажали ${this.state.count} раз`
}
componentDidUpdate() {
document.title = `Вы нажали ${this.state.count} раз`
}
render() {
return (
<div>
<p>Вы нажали {this.state.count} раз</p>
<button
onClick={() =>
this.setState({ count: this.state.count + 1 })
}
>
Нажми на меня
</button>
</div>
)
}
}
Обратите внимание, что нам приходится дублировать наш код между этими классовыми методами жизненного цикла.
Это всё потому, что во многих случаях, мы хотим выполнять одни и те же побочные эффекты вне зависимости от того, был ли компонент только что смонтирован или обновлён. Мы хотим чтобы они выполнялись после каждого рендера — но у классовых React-компонентов нет таких встроенных методов. Мы могли бы вынести этот метод отдельно, но нам бы всё равно пришлось бы вызывать его в двух местах.
А сейчас, давайте рассмотрим, как мы можем сделать то же самое с использованием хука useEffect
.
Пример с использованием хуков¶
Мы уже рассматривали этот пример немного ранее, но давайте разберём его более подробно:
import React, { useState, useEffect } from 'react'
function Example() {
const [count, setCount] = useState(0)
useEffect(() => {
document.title = `Вы нажали ${count} раз`
})
return (
<div>
<p>Вы нажали {count} раз</p>
<button onClick={() => setCount(count + 1)}>
Нажми на меня
</button>
</div>
)
}
Что же делает useEffect
? Используя этот хук, вы говорите React сделать что-то после рендера. React запомнит функцию (то есть «эффект»), которую вы передали и вызовет её после того, как внесёт все изменения в DOM. В этом эффекте мы устанавливаем заголовок документа, но мы также можем выполнить запрос данных или вызвать какой-нибудь императивный API.
Почему же мы вызываем useEffect
непосредственно внутри компонента? Это даёт нам доступ к переменной состояния count
(или любым другим пропсам) прямиком из эффекта. Нам не нужен специальный API для доступа к этой переменной — она уже находится у нас в области видимости функции. Хуки используют JavaScript-замыкания, и таким образом, им не нужен специальный для React API, поскольку сам JavaScript уже имеет готовое решение для этой задачи.
Выполняется ли useEffect
после каждого рендера? Разумеется! По умолчанию он будет выполняться после каждого рендера и обновления. Мы рассмотрим, как настраивать это немного позже. Вместо того, чтобы воспринимать это с позиции «монтирования» и «обновления», мы советуем просто иметь в виду, что эффекты выполняются после каждого рендера. React гарантирует, что он запустит эффект только после того, как DOM уже обновился.
Подробное объяснение¶
Мы узнали немного больше о принципе работы эффектов и теперь этот код уже вовсе не кажется таким непонятным:
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Вы нажали ${count} раз`;
});
Мы объявляем переменную состояния count
и говорим React, что мы хотим использовать эффект. Далее, мы передаём функцию в хук useEffect
. Эта функция как раз и будет нашим эффектом. Внутри этого эффекта мы устанавливаем заголовок документа, используя API браузера document.title
. Мы можем получать доступ к актуальной переменной count
изнутри эффекта, так как он находится в области видимости нашей функции. Когда React рендерит наш компонент, он запоминает эффект, который мы использовали, и запускает его после того, как обновит DOM. Это будет происходить при каждом рендере, в том числе и при первоначальном.
Опытные JavaScript-разработчики могут подметить, что функция, которую мы передаём в useEffect
, будет меняться при каждом рендере. На самом деле, это было сделано преднамеренно. Это как раз то, что даёт нам возможность получать актуальную версию переменной count
изнутри эффекта, не беспокоясь о том, что её значение устареет. Каждый раз при повторном рендере, мы ставим в очередь новый эффект, который заменяет предыдущий. В каком-то смысле, это включает поведение эффектов как часть результата рендера, то есть каждый эффект «принадлежит» определённому рендеру. Мы расскажем о преимуществах данного подхода далее на этой странице.
Совет
В отличие от
componentDidMount
илиcomponentDidUpdate
, эффекты, запланированные с помощьюuseEffect
, не блокируют браузер при попытке обновить экран. Ваше приложение будет быстрее реагировать на действия пользователя, даже когда эффект ещё не закончился. Большинству эффектов не нужно работать в синхронном режиме. Есть редкие случаи, когда им всё же нужно это делать (например, измерять раскладку), но для этого мы разработали специальный хукuseLayoutEffect
с точно таким же API, как и уuseEffect
.
Эффекты со сбросом¶
Ранее мы рассмотрели побочные эффекты, которые не требуют сброса. Однако, есть случаи, когда сброс всё же необходим. Например, нам может потребоваться установить подписку на какой-нибудь внешний источник данных. В этом случае очень важно выполнять сброс, чтобы не случилось утечек памяти! Давайте сравним, как мы можем это реализовать с помощью классов и хуков.
Пример с использованием классов¶
В React-классе, вы, как правило, оформили бы подписку в componentDidMount
и отменили бы её в componentWillUnmount
. Например, предположим, что у нас есть некий модуль ChatAPI
, с помощью которого мы можем подписаться на статус друга в сети. Вот как мы бы подписались и отобразили бы статус, используя класс:
class FriendStatus extends React.Component {
constructor(props) {
super(props)
this.state = { isOnline: null }
this.handleStatusChange = this.handleStatusChange.bind(
this
)
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
)
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
)
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline,
})
}
render() {
if (this.state.isOnline === null) {
return 'Загрузка...'
}
return this.state.isOnline ? 'В сети' : 'Не в сети'
}
}
Обратите внимание, что componentDidMount
и componentWillUnmount
по сути содержат идентичный код. Методы жизненного цикла вынуждают нас разбивать эту логику, хоть и фактически код обоих методов относится к одному и тому же эффекту.
Примечание
Вы могли заметить, что для правильной работы, нашему компоненту также нужен
componentDidUpdate
. Мы вернёмся к этому моменту ниже на этой странице.
Пример с использованием хуков¶
Давайте рассмотрим, как этот компонент будет выглядеть, если написать его с помощью хуков.
Вы должно быть подумали, что нам потребуется отдельный эффект для выполнения сброса. Так как код для оформления и отмены подписки тесно связан с useEffect
, мы решили объединить их. Если ваш эффект возвращает функцию, React выполнит её только тогда, когда наступит время сбросить эффект.
import React, { useState, useEffect } from 'react'
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null)
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline)
}
ChatAPI.subscribeToFriendStatus(
props.friend.id,
handleStatusChange
)
// Указываем, как сбросить этот эффект:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(
props.friend.id,
handleStatusChange
)
}
})
if (isOnline === null) {
return 'Загрузка...'
}
return isOnline ? 'В сети' : 'Не в сети'
}
Зачем мы вернули функцию из нашего эффекта? Это необязательный механизм сброса эффектов. Каждый эффект может возвратить функцию, которая сбросит его. Это даёт нам возможность объединить вместе логику оформления и отмены подписки. Они, всё-таки, часть одного и того же эффекта!
Когда именно React будет сбрасывать эффект? React будет сбрасывать эффект перед тем, как компонент размонтируется. Однако, как мы уже знаем, эффекты выполняются не один раз, а при каждом рендере. Вот почему React также сбрасывает эффект из предыдущего рендера, перед тем, как запустить следующий. Мы рассмотрим почему это позволяет избежать багов и как отказаться от этой логики, если это вызывает проблемы с производительностью далее.
Совет
Нам не нужно возвращать именованную функцию из эффекта. Мы назвали её «сбросом», чтобы объяснить её предназначение. Вы можете по желанию возвратить стрелочную функцию или назвать её как-то по-другому.
Итог¶
Мы узнали, что с помощью useEffect
, мы можем вызывать разные побочные эффекты после того, как компонент отрендерится. Некоторым эффектам нужен сброс, поэтому они возвращают соответствующую функцию.
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline)
}
ChatAPI.subscribeToFriendStatus(
props.friend.id,
handleStatusChange
)
return () => {
ChatAPI.unsubscribeFromFriendStatus(
props.friend.id,
handleStatusChange
)
}
})
В некоторых эффектах нет этапа сброса, поэтому они не возвращают ничего.
useEffect(() => {
document.title = `Вы нажали ${count} раз`
})
Хук эффекта покрывает оба сценария единым API.
Если вы чувствуете, что вы достаточно разобрались с тем, как работает хук эффекта, вы можете отправиться на страницу о правилах хуков прямо сейчас.
Советы по использованию эффектов¶
Сейчас, давайте углубимся в некоторые особенности хука useEffect
, о которых опытные пользователи React наверняка уже задумались. Пожалуйста, не заставляйте себя углубляться в эти особенности прямо сейчас. Вы можете сперва закрепить выше пройденный материал и вернуться сюда позже в любой момент.
Совет: используйте разные хуки для разных задач¶
Один из ключевых моментов, которые мы описали в мотивации, приводит аргументы о том, что в отличии от хуков, классовые методы жизненного цикла часто содержат логику, которая никак между собой не связана, в то время как связанная логика, разбивается на несколько методов. Далее мы приведём пример компонента, который объединяет в себе логику счётчика и индикатора статуса нашего друга из предыдущих примеров:
class FriendStatusWithCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0, isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
document.title = `Вы нажали ${this.state.count} раз`;
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate() {
document.title = `Вы нажали ${this.state.count} раз`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
// ...
Обратите внимание, что логика, которая устанавливает document.title
разбита между componentDidMount
и componentDidUpdate
. Логика подписки также раскидана между componentDidMount
и componentWillUnmount
. А метод componentDidMount
включает в себя логику для обеих задач.
Как же можно решить эту проблему с помощью хуков? Точно так же, как вы можете использовать хук состояния более одного раза, вы можете использовать и несколько эффектов. Это даёт нам возможность разделять разную несвязанную между собой логику между разными эффектами.
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0)
useEffect(() => {
document.title = `Вы нажали ${count} раз`
})
const [isOnline, setIsOnline] = useState(null)
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline)
}
ChatAPI.subscribeToFriendStatus(
props.friend.id,
handleStatusChange
)
return () => {
ChatAPI.unsubscribeFromFriendStatus(
props.friend.id,
handleStatusChange
)
}
})
// ...
}
С помощью хуков, мы можем разделить наш код основываясь на том, что он делает, а не по принципам методов жизненного цикла. React будет выполнять каждый используемый эффект в компоненте, согласно порядку их объявления.
Объяснение: почему эффекты выполняются при каждом обновлении¶
Если вы привыкли пользоваться классами, вам может быть не совсем ясно, почему этап сброса эффекта происходит после каждого последующего рендера, а не один лишь раз во время размонтирования. Давайте рассмотрим на практике, почему такой подход помогает создавать компоненты с меньшим количеством багов.
Ранее на этой странице, мы рассматривали пример с компонентом FriendStatus
, который отображает в сети наш друг или нет. Наш класс берёт friend.id
из this.props
, подписывается на статус друга после того, как компонент смонтировался, и отписывается во время размонтирования.
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
Но что же произойдёт, если проп friend
поменяется, пока компонент все ещё находится на экране? Наш компонент будет отображать статус в сети уже какого-нибудь другого друга. Это как раз таки баг. Это также может привести к утечки памяти или вообще к вылету нашего приложения при размонтировании, так как метод отписки будет использовать неправильный ID друга, от которого мы хотим отписаться.
В классовом компоненте нам бы пришлось добавить componentDidUpdate
, чтобы решить эту задачу:
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) {
// Отписаться от предыдущего friend.id
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// Подписаться на следующий friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
Не использовать componentDidUpdate
надлежащим образом — это один из самых распространённых источников багов в приложениях React.
Теперь давайте рассмотрим версию этого же компонента, но уже написанного с использованием хуков:
function FriendStatus(props) {
// ...
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
Этого бага в данном компоненте нет. (Но мы и не изменили там ничего)
Здесь нет никакого особого кода для решения проблем с обновлениями, так как useEffect
решает их по умолчанию. Он сбрасывает предыдущие эффекты прежде чем выполнить новые. Чтобы показать это на практике, давайте рассмотрим последовательность подписок и отписок, которые этот компонент может выполнить в течение некоторого времени.
// Монтируем с пропсами { friend: { id: 100 } }
ChatAPI.subscribeToFriendStatus(100, handleStatusChange) // Выполняем первый эффект
// Обновляем с пропсами { friend: { id: 200 } }
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange) // Сбрасываем предыдущий эффект
ChatAPI.subscribeToFriendStatus(200, handleStatusChange) // Выполняем следующий эффект
// Обновляем с пропсами { friend: { id: 300 } }
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange) // Сбрасываем предыдущий эффект
ChatAPI.subscribeToFriendStatus(300, handleStatusChange) // Выполняем следующий эффект
// Размонтируем
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange) // Сбрасываем последний эффект
Эта логика по умолчанию гарантирует согласованность выполняемых нами действий и исключает баги, распространённые в классовых компонентах из-за упущенной логики обновления.
Совет: оптимизация производительности за счёт пропуска эффектов¶
В некоторых случаях сброс или выполнение эффекта при каждом рендере может вызвать проблему с производительностью. В классовых компонентах, мы можем решить это используя дополнительное сравнение prevProps
или prevState
внутри componentDidUpdate
:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `Вы нажали ${this.state.count} раз`;
}
}
Эту логику приходится использовать довольно часто, поэтому мы решили встроить её в API хука useEffect
. Вы можете сделать так, чтобы React пропускал вызов эффекта, если определённые значения остались без изменений между последующими рендерами. Чтобы сделать это, передайте массив в useEffect
вторым необязательным аргументом.
useEffect(() => {
document.title = `Вы нажали ${count} раз`
}, [count]) // Перезапускать эффект только если count поменялся
В этом примере, мы передаём [count]
вторым аргументом. Что это вообще значит? Это значит, что если count
будет равен 5
и наш компонент повторно рендерится с тем же значением count
= 5
, React сравнит [5]
из предыдущего рендера и [5]
из следующего рендера. Так как, все элементы массива остались без изменений (5 === 5
), React пропустит этот эффект. Это и есть оптимизация данного процесса.
Когда при следующем рендере наша переменная count
обновится до 6
, React сравнит элементы в массиве [5]
из предыдущего рендера и элементы массива [6]
из следующего рендера. На этот раз, React выполнит наш эффект, так как 5 !== 6
. Если у вас будет несколько элементов в массиве, React будет выполнять наш эффект, в том случае, когда хотя бы один из них будет отличаться.
Это также работает для эффектов с этапом сброса:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline)
}
ChatAPI.subscribeToFriendStatus(
props.friend.id,
handleStatusChange
)
return () => {
ChatAPI.unsubscribeFromFriendStatus(
props.friend.id,
handleStatusChange
)
}
}, [props.friend.id]) // Повторно подписаться, только если props.friend.id изменился
В будущем, второй аргумент возможно будет добавляться автоматически с помощью трансформации во время выполнения.
Примечание
Если вы хотите использовать эту оптимизацию, обратите внимание на то, чтобы массив включал в себя все значения из области видимости компонента (такие как пропсы и состояние), которые могут изменяться с течением времени, и которые будут использоваться эффектом. В противном случае, ваш код будет ссылаться на устаревшее значение из предыдущих рендеров. Отдельные страницы документации рассказывают о том, как поступить с функциями и что делать с часто изменяющимися массивами.
Если вы хотите запустить эффект и сбросить его только один раз (при монтировании и размонтировании), вы можете передать пустой массив (
[]
) вторым аргументом. React посчитает, что ваш эффект не зависит от каких-либо значений из пропсов или состояния и поэтому не будет выполнять повторных рендеров. Это не обрабатывается как особый случай — он напрямую следует из логики работы массивов зависимостей.Если вы передадите пустой массив (
[]
), пропсы и состояние внутри эффекта всегда будут иметь значения, присвоенные им изначально. Хотя передача[]
ближе по модели мышления к знакомымcomponentDidMount
иcomponentWillUnmount
, обычно есть более хорошие способы избежать частых повторных рендеров. Не забывайте, что React откладывает выполнениеuseEffect
, пока браузер не отрисует все изменения, поэтому выполнение дополнительной работы не является существенной проблемой.Мы рекомендуем использовать правило
exhaustive-deps
, входящее в наш пакет правил линтераeslint-plugin-react-hooks
. Оно предупреждает, когда зависимости указаны неправильно и предлагает исправление.
Следующие шаги¶
Поздравляем! Это была длинная страница, но мы надеемся, что под конец, у нас получилось ответить на все ваши вопросы по поводу работы эффектов. Вы уже узнали о хуке состояния и о хуке эффекта, и теперь есть очень много вещей, которые вы можете делать, объединив их вместе. Они покрывают большинство задач решаемых классами. В остальных случаях, вам могут пригодиться дополнительные хуки.
Мы также узнали, как хуки избавляют от проблем описанных в мотивации. Мы увидели, как с помощью сброса эффектов нам удаётся избежать повторов кода в componentDidUpdate
и componentWillUnmount
, объединить связанный код вместе и защитить наш код от багов. Мы также рассмотрели, как можно разделять наши эффекты по смыслу и назначению, что ранее было невозможно в классах.
На этом этапе, вы, возможно, задаётесь вопросом, как хуки работают в целом. Как React понимает, какая переменная состояния соответствует какому вызову useState
между повторными рендерами? Как React «сопоставляет» предыдущие и следующие эффекты при каждом обновлении? На следующей странице, мы узнаем о правилах хуков, так как они являются залогом правильной работы хуков.
Последнее обновление: 01.04.2022
Хук useEffect позволяет управлять различными сопутствующими действиями в функциональном компоненте
или то, что называется «side effects» (побочные эффекты), например, извлечение данных, ручное изменение структуры DOM, использование таймеров,
логгирование и т.д.. То есть в useEffect
выполняет те действия, которые мы не можем выполнить в основной части функционального компонента.
Этот хук фактически служит той же цели, что методы жизненного цикла componentDidMount, componentDidUpdate
и componentWillUnmount
в классах-компонентах.
В качестве параметра в useEffect()
передается функция. При вызове хука useEffect
по сути определяется «эффект», который затем применяется в приложении.
Когда именно применяется? По умолчанию React применяет эффект после каждого рендеринга, в том числе при первом рендеринге приложения.
Причем поскольку подобные эффекты определены внутри компонента, они имеют доступ к объекту props
и к состоянию компонента.
Например, изменение структуры DOM через useEffect
:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <div id="app"></div> <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <script type="text/babel"> function User() { const [name, setName] = React.useState("Tom"); React.useEffect(() => { // Изменяем заголовок html-страницы document.title = `Привет ${name}`; }); function changeName(event) { setName(event.target.value); } return ( <div> <h3>Имя: {name}</h3> <div> <p>Имя: <input type="text" value={name} onChange={changeName} /></p> </div> </div> ); } ReactDOM.createRoot( document.getElementById("app") ) .render( <User /> ); </script> </body> </html>
Здесь мы определяем эффект, который изменяет заголовок страницы. Причем в заголовок выводится значение переменной состояния — переменной name
.
То есть при загрузке страницы мы увидим в ее заголовке «Привет Tom».
Однако поскольку при вводе в текстовое поле мы изменяем значение в переменной name, и соответственно React будет выполнять
перерендеринг приложения, то одновременно с этим будет изменяться и заголовок страницы:
Ограничение применения эффекта
По умолчанию эффект выполняется при каждом повторном рендеринге на веб-странице, однако мы можем указать, чтобы
React не применял эффект, если определенные значения не изменились между с момента последнего рендеринга. Для этого в useEffect в качестве необязательного параметра
передается массив аргументов.
Сначала рассмотрим саму проблему применения эффекта. Допустим у нас есть следующая страница:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <div id="app"></div> <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <script type="text/babel"> function User() { const [name, setName] = React.useState("Tom"); const [age, setAge] = React.useState(36); React.useEffect(() => { // Изменяем заголовок html-страницы document.title = `Привет ${name}`; console.log("useEffect"); }); const changeName = (event) => setName(event.target.value); const changeAge =(event) => setAge(event.target.value); return ( <div> <h3>Имя: {name}</h3> <h3>Возраст: {age}</h3> <div> <p>Имя: <input type="text" value={name} onChange={changeName} /></p> <p>Возраст: <input type="number" value={age} onChange={changeAge} /></p> </div> </div> ); } ReactDOM.createRoot( document.getElementById("app") ) .render( <User /> ); </script> </body> </html>
В данном случае в компоненте определены две переменных состояния: name и age. При этом эффект использует только переменную name. Однако даже
если переменная name останется без изменений, но переменная age будет изменена, в этом случае эффект будет повторно срабатывать:
Это не самое желательное поведение, в котором нет никакого смысла. И чтобы указать, что эффект применяется только при изменении переменной name,
передадим ее в качестве необязательного параметра в функцию useEffect
:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <div id="app"></div> <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <script type="text/babel"> function User() { const [name, setName] = React.useState("Tom"); const [age, setAge] = React.useState(36); React.useEffect(() => { // Изменяем заголовок html-страницы document.title = `Привет ${name}`; console.log("useEffect"); }, [name]); // эффект срабатывает только при изменении name const changeName = (event) => setName(event.target.value); const changeAge =(event) => setAge(event.target.value); return ( <div> <h3>Имя: {name}</h3> <h3>Возраст: {age}</h3> <div> <p>Имя: <input type="text" value={name} onChange={changeName} /></p> <p>Возраст: <input type="number" value={age} onChange={changeAge} /></p> </div> </div> ); } ReactDOM.createRoot( document.getElementById("app") ) .render( <User /> ); </script> </body> </html>
Если мы хотим, чтобы эффект вызывался только один раз при самом первом рендеринге, то в качестве параметра передаются пустые
квадратные скобки — []
.
React.useEffect(() => { // Изменяем заголовок html-страницы document.title = `Привет ${name}`; console.log("useEffect"); }, []); // эффект срабатывает только один раз - при самом первом рендеринге
Очистка ресурсов
Нередко в приложении возникает необходимость подисывается на различные ресурсы, а после окончания работы и отписываться от них.
В этом случае мы можем использовать специальную форму хука useEffect()
:
useEffect(() => { // код подписки на ресурс return () => { // код отписки от ресурса }; });
В начале функции хука идет подписка на ресурс, а далее оператор return
возвращает функцию, которая выполняет отписку от ресурса.
В качестве примера используем подписку/отписку на событие кнопки:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <div id="app"></div> <button id="unmountBtn">Unmount</button> <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <script type="text/babel"> const root = ReactDOM.createRoot( document.getElementById("app") ); function User() { const [name, setName] = React.useState("Tom"); const unmount = () => root.unmount(); React.useEffect(() => { const unmountBtn = document.getElementById("unmountBtn"); // подписываемся на событие onclick кнопки unmountBtn unmountBtn.addEventListener("click", unmount); console.log("EventListener added"); return()=>{ // отписываемся от события unmountBtn.removeEventListener("click", unmount); console.log("EventListener removed"); } }, []); // эффект срабатывает только один раз - при самом первом рендеринге return (<div> <h3>Имя: {name}</h3> <p>Имя: <input type="text" value={name} onChange={(e) => setName(e.target.value)} /></p> </div>); } root.render( <User /> ); </script> </body> </html>
Итак, на странице определеная кнопка с id=unmountBtn
, на событие которой мы будем подписываться. В качестве действия, которое будет выполнять кнопка,
в компоненте User определена функция unmount()
, которая удаляет данный компонент с веб-страницы:
root.unmount();
В хуке useEffect
сначала подписываемся на событие «click» кнопки (то есть на событие нажатия):
unmountBtn.addEventListener("click", unmount);
Оператор return
возвращает функцию, которая выполняет отписку от события:
unmountBtn.removeEventListener("click", unmount);
И теперь важный момент: хук useEffect
в данном случае будет срабатывать один раз — при самом первом рендеринге приложения —
для этого в функцию в качестве необязательного параметра передается пустой массив. И соответственно функция очистки ресурсов, которая возвращаается
оператором return будет выполняться один раз — при удалении компонента с веб-страницы.
React.useEffect(() => { //............................ return()=>{ //................. } }, []); // пустой массив - хук выполняется один раз
То есть получится следущее: при загрузке компонент подисывается на событие кнопки. По нажатию на кнопку удаляется компонент. При удалении
в хуке происходит удаление подписки — она нам больше не нужна, так как компонент уже удален.
Editor’s note: This article was last updated on 9 February 2023. For more information on React Hooks, check out this cheat sheet.
Understanding how the useEffect
Hook works is one of the most important concepts for mastering React today. Suppose you have been working with React for several years. In that case, it is especially crucial to understand how working with useEffect
differs from working with the lifecycle methods of class-based components.
Fully understanding effects is a complex issue. According to Dan Abramov of the React team, you might have to unlearn some things to fully grasp effects.
With useEffect
, you invoke side effects from within functional components, which is an important concept to understand in the React Hooks era. Working with the side effects invoked by the useEffect
Hook may seem cumbersome at first, but you’ll eventually everything will make sense.
The goal of this article is to gather information about the underlying concepts of useEffect
and, in addition, to provide learnings from my own experience with the useEffect
Hook. The code snippets provided are part of my companion GitHub project.
Jump ahead:
- The core concepts of
useEffect
- The key concepts of using effects
- Using
useEffect
for asynchronous tasks - Using multiple effects to separate concerns
- When are effects executed within the component lifecycle?
- The
useEffect
control flow at a glance - How to execute side effects with
useEffect
- The importance of the dependency array
- The rules of Hooks
- What are dependency array items?
- Utilizing cleanup functions
- Implications of prop and state changes
- More on prop changes and using the
useCallback
Hook useCallback
withuseContext
useEffect
inside of custom Hooks- Additional thoughts on functions used inside of effects
- Using async functions inside of
useEffect
- Unit testing of effects
- Some more
useEffect
receipts - When not to use
useEffect
The core concepts of useEffect
What are the effects, really? Examples are:
- Fetching data
- Reading from local storage
- Registering and deregistering event listeners
React’s effects are a completely different animal than the lifecycle methods of class-based components. The abstraction level differs, too.
“I’ve found Hooks to be a very powerful abstraction — possibly a little too powerful. As the saying goes, with great power comes great responsibility.”
– Bryan Manuele
To their credit, lifecycle methods do give components a predictable structure. The code is more explicit in contrast to effects, so developers can directly spot the relevant parts (e.g., componentDidMount
) in terms of performing tasks at particular lifecycle phases (e.g., on component unmount).
As we will see later, the useEffect
Hook fosters the separation of concerns and reduces code duplication. For example, the official React docs show that you can avoid the duplicated code that results from lifecycle methods with one useEffect
statement.
The key concepts of using effects
Before we continue, we should summarize the main concepts you’ll need to understand to master useEffect
. Throughout the article, I will highlight the different aspects in great detail:
- You must thoroughly understand when components (re-)render because effects run after every render cycle
- Effects are always executed after rendering, but you can opt-out of this behavior
- You must understand basic JavaScript concepts about values to opt out or skip effects. An effect is only rerun if at least one of the values specified as part of the effect’s dependencies has changed since the last render cycle
- You should ensure that components are not re-rendered unnecessarily. This constitutes another strategy to skip unnecessary reruns of effects.
- You have to understand that functions defined in the body of your function component get recreated on every render cycle. This has an impact if you use it inside of your effect. There are strategies to cope with it (hoist them outside of the component, define them inside of the effect, use
useCallback
) - You have to understand basic JavaScript concepts such as stale closures, otherwise, you might have trouble tackling problems with outdated props or state values inside of your effect. There are strategies to solve this, e.g., with an effect’s dependency array or with the
useRef
Hook - You should not ignore suggestions from the React Hooks ESLint plugin. Do not blindly remove dependencies or carelessly use ESLint’s disable comments; you most likely have introduced a bug. You may still lack understanding of some important concepts
- Do not mimic the lifecycle methods of class-based components. This way of thinking does more harm than good. Instead, think more about data flow and state associated with effects because you run effects based on state changes across render cycles
The following tweet provides an excellent way to think about the last bullet point:
“The question is not ‘when does this effect run,’ the question is ‘with which state does this effect synchronize?’ ”
– Ryan Florence
Using useEffect
for asynchronous tasks
For your fellow developers, useEffect
code blocks are clear indicators of asynchronous tasks. Of course, it is possible to write asynchronous code without useEffect
, but it is not the “React way,” and it increases both complexity and the likelihood of introducing errors.
Instead of writing asynchronous code without useEffect
that might block the UI, utilizing useEffect
is a known pattern in the React community — especially the way the React team has designed it to execute side effects.
Another advantage of using useEffect
is that developers can easily overview the code and quickly recognize code that is executed “outside the control flow,” which becomes relevant only after the first render cycle.
On top of that, useEffect
blocks are candidates to extract into reusable and even more semantic custom Hooks.
Using multiple effects to separate concerns
Don’t be afraid to use multiple useEffect
statements in your component. While useEffect
is designed to handle only one concern, you’ll sometimes need more than one effect.
When you try to use only one effect for multiple purposes, it decreases the readability of your code, and some use cases are not realizable.
When are effects executed within the component lifecycle?
First, a reminder: don’t think in lifecycle methods anymore! Don’t try to mimic these methods! I will go into more detail about the motives later.
This interactive diagram shows the React phases in which certain lifecycle methods (e.g., componentDidMount
) are executed:
In contrast, the next diagram shows how things work in the context of functional components:
This may sound strange initially, but effects defined with useEffect
are invoked after render. To be more specific, it runs both after the first render and after every update. In contrast to lifecycle methods, effects don’t block the UI because they run asynchronously.
If you are new to React, I would recommend ignoring class-based components and lifecycle methods and, instead, learning how to develop functional components and how to decipher the powerful possibilities of effects. Class-based components are rarely used in more recent React development projects.
If you are a seasoned React developer and are familiar with class-based components, you have to do some of the same things in your projects today as you did a few years ago when there were no Hooks.
For example, it is pretty common to “do something” when the component is first rendered. The difference with Hooks here is subtle: you do not do something after the component is mounted; you do something after the component is first presented to the user. As others have noted, Hooks force you to think more from the user’s perspective.
The useEffect
control flow at a glance
This section briefly describes the control flow of effects. The following steps are carried out for a functional React component if at least one effect is defined:
- The component will be re-rendered based on a state, prop, or context change
- If one or more
useEffect
declarations exist for the component, React checks eachuseEffect
to determine whether it fulfills the conditions to execute the implementation (the body of the callback function provided as first argument). In this case, “conditions” mean one or more dependencies have changed since the last render cycle
Dependencies are array items provided as the optional second argument of the useEffect
call. Array values must be from the component scope (i.e., props, state, context, or values derived from the aforementioned):
- After the execution of every effect, scheduling of new effects occurs based on every effect’s dependencies. If an effect does not specify a dependency array at all, it means that this effect is executed after every render cycle
- Cleanup is an optional step for every effect if the body of the
useEffect
callback function (first argument) returns a so-called “cleanup callback function.” In this case, the cleanup function gets invoked before the execution of the effect, beginning with the second scheduling cycle. This also means that if there is no second execution of an effect scheduled, the cleanup function is invoked before the React component gets destroyed.
I am quite sure that this lifecycle won’t be entirely clear to you if you have little experience with effects. That’s why I explain every aspect in great detail throughout this article. I encourage you to return to this section later — I’m sure your next read will be totally clear.
How to execute side effects with useEffect
The signature of the useEffect
Hook looks like this:
useEffect( () => { // execute side effect }, // optional dependency array [ // 0 or more entries ] )
Because the second argument is optional, the following execution is perfectly fine:
useEffect(() => { // execute side effect })
Let’s take a look at an example. The user can change the document title with an input field:
import React, { useState, useRef, useEffect } from "react"; function EffectsDemoNoDependency() { const [title, setTitle] = useState("default title"); const titleRef = useRef(); useEffect(() => { console.log("useEffect"); document.title = title; }); const handleClick = () => setTitle(titleRef.current.value); console.log("render"); return ( <div> <input ref={titleRef} /> <button onClick={handleClick}>change title</button> </div> ); }
The useEffect
statement is only defined with a single, mandatory argument to implement the actual effect to execute. In our case, we use the state variable representing the title and assign its value to document.title
.
Because we skipped the second argument, this useEffect
is called after every render. Because we implemented an uncontrolled input field with the help of the useRef
Hook, handleClick
is only invoked after the user clicks on the button. This causes a re-render because setTitle
performs a state change.
After every render cycle, useEffect
is executed again. To demonstrate this, I added two console.log
statements:
The first two log outputs are due to the initial rendering after the component was mounted. Let’s add another state variable to the example to toggle a dark mode with the help of a checkbox:
function EffectsDemoTwoStates() { const [title, setTitle] = useState("default title"); const titleRef = useRef(); const [darkMode, setDarkMode] = useState(false); useEffect(() => { console.log("useEffect"); document.title = title; }); console.log("render"); const handleClick = () => setTitle(titleRef.current.value); const handleCheckboxChange = () => setDarkMode((prev) => !prev); return ( <div className={darkMode ? "dark-mode" : ""}> <label htmlFor="darkMode">dark mode</label> <input name="darkMode" type="checkbox" checked={darkMode} onChange={handleCheckboxChange} /> <input ref={titleRef} /> <button onClick={handleClick}>change title</button> </div> ); }
However, this example leads to unnecessary effects when you toggle the darkMode
state variable:
Of course, it’s not a huge deal in this example, but you can imagine more problematic use cases that cause bugs or, at least, performance issues. Let’s take a look at the following code and try to read the initial title from local storage, if available, in an additional useEffect
block:
function EffectsDemoInfiniteLoop() { const [title, setTitle] = useState("default title"); const titleRef = useRef(); useEffect(() => { console.log("useEffect title"); document.title = title; }); useEffect(() => { console.log("useEffect local storage"); const persistedTitle = localStorage.getItem("title"); setTitle(persistedTitle || []); }); console.log("render"); const handleClick = () => setTitle(titleRef.current.value); return ( <div> <input ref={titleRef} /> <button onClick={handleClick}>change title</button> </div> ); }
As you can see, we have an infinite loop of effects because every state change with setTitle
triggers another effect, which updates the state again:
The importance of the dependency array
Let’s go back to our previous example with two states (title and dark mode). Why do we have the problem of unnecessary effects?
Again, if you do not provide a dependency array, every scheduled useEffect
is executed. This means that after every render cycle, every effect defined in the corresponding component is executed one after the other based on the positioning in the source code.
So the order of your effect definitions matter. In our case, our single useEffect
statement is executed whenever one of the state variables change.
You have the ability to opt out from this behavior. This is managed with dependencies you provide as array entries. In these cases, React only executes the useEffect
statement if at least one of the provided dependencies has changed since the previous run. In other words, with the dependency array, you make the execution dependent on certain conditions.
More often than not, this is what we want; we usually want to execute side effects after specific conditions, e.g., data has changed, a prop changed, or the user first sees our component. Another strategy to skip unnecessary effects is to prevent unnecessary re-renders in the first place with, for example, React.memo
, as we’ll see later.
Back to our example where we want to skip unnecessary effects after an intended re-render. We just have to add an array with title
as a dependency. With that, the effect is only executed when the values between render cycles differ:
useEffect(() => { console.log("useEffect"); document.title = title; }, [title]);
Here’s the complete code snippet:
function EffectsDemoTwoStatesWithDependeny() { const [title, setTitle] = useState("default title"); const titleRef = useRef(); const [darkMode, setDarkMode] = useState(false); useEffect(() => { console.log("useEffect"); document.title = title; }, [title]); console.log("render"); const handleClick = () => setTitle(titleRef.current.value); const handleCheckboxChange = () => setDarkMode((prev) => !prev); return ( <div className={darkMode ? "view dark-mode" : "view"}> <label htmlFor="darkMode">dark mode</label> <input name="darkMode" type="checkbox" checked={darkMode} onChange={handleCheckboxChange} /> <input ref={titleRef} /> <button onClick={handleClick}>change title</button> </div> ); }
As you can see in the recording, effects are only invoked as expected on pressing the button:
It’s also possible to add an empty dependency array. In this case, effects are only executed once; it is similar to the componentDidMount()
lifecycle method. To demonstrate this, let’s take a look at the previous example with the infinite loop of effects:
function EffectsDemoEffectOnce() { const [title, setTitle] = useState("default title"); const titleRef = useRef(); useEffect(() => { console.log("useEffect title"); document.title = title; }); useEffect(() => { console.log("useEffect local storage"); const persistedTitle = localStorage.getItem("title"); setTitle(persistedTitle || []); }, []); console.log("render"); const handleClick = () => setTitle(titleRef.current.value); return ( <div> <input ref={titleRef} /> <button onClick={handleClick}>change title</button> </div> ); }
We just added an empty array as our second argument. Because of this, the effect is only executed once after the first render and skipped for the following render cycles:
If you think about it, this behavior makes sense. In principle, the dependency array says, “Execute the effect provided by the first argument after the next render cycle whenever one of the arguments changes.” However, we don’t have any argument, so dependencies will never change in the future.
That’s why using an empty dependency array makes React invoke an effect only once — after the first render. The second render along with the second useEffect title
is due to the state change invoked by setTitle()
after we read the value from local storage.
The rules of Hooks
Before we continue with more examples, we have to talk about the general rules of Hooks. These are not exclusive to the useEffect
Hook, but it’s important to understand at which places in your code you can define effects. You need to follow rules to use Hooks:
- Hooks can only be invoked from the top-level function constituting your functional React component
- Hooks may not be called from nested code (e.g., loops, conditions, or another function body)
- Custom Hooks are special functions, however, and Hooks may be called from the top-level function of the custom Hook. In addition, rule two is also true
How the React Hooks ESLint plugin promotes understanding of the rules of Hooks
There’s a handy ESLint plugin that assists you in following the rules of Hooks. It lets you know if you violate one of the rules:
In addition, it helps you to provide a correct dependency array for effects in order to prevent bugs:
This plugin is great because, in practice, you might miss the opportunity to add dependencies to the list; this is not always obvious at firstI like the plugin because its messages foster learning more about how effects work.
If you don’t understand why the plugin wants you to add a specific dependency, please don’t prematurely ignore it! You should at least have an excellent explanation for doing so. I have recently discovered that, in some circumstances, you most likely will have a bug if you omit the dependency
useEffect(() => { // ... // eslint-disable-next-line react-hooks/exhaustive-deps }, []);
Finally, be aware that the plugin is not omniscient. You have to accept that the ESLint plugin cannot understand the runtime behavior of your code. It can only apply static code analysis. There are certainly cases where the plugin cannot assist you.
However, I have no arguments against integrating the plugin into your project setup. It reduces error-proneness and increases robustness. In addition, take a closer look at the provided suggestions; they might enable new insights into concepts you haven’t grasped completely.
That said, you shouldn’t be as dogmatic as always to satisfy the plugin. Check out the setup in the companion project for this article.
What are dependency array items?
This brings us to an important question: What items should be included in the dependency array? According to the React docs, you must include all values from the component scope that change their values between re-renders.
What does this mean, exactly? All external values referenced inside of the useEffect
callback function, such as props, state variables, or context variables, are dependencies of the effect. Ref containers (i.e., what you directly get from useRef()
and not the current
property) are also valid dependencies. Even local variables, which are derived from the aforementioned values, have to be listed in the dependency array.
It is essential to understand the conceptual thinking of effects; the React team wants you to treat every value used inside of the effect as dynamic. So even if you use a non-function value inside the effect and are pretty sure this value is unlikely to change, you should include the value in the dependency array.
Therefore, make sure to add every value from the component scope to the list of dependencies because you should treat every value as mutable. Remember that if at least one of the dependencies in the array is different from the previous render, the effect will be rerun.
Utilizing cleanup functions
The next snippet shows an example to demonstrate a problematic issue:
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const interval = setInterval(function () { setCount((prev) => prev + 1); }, 1000); }, []); return <p>and the counter counts {count}</p>; } function EffectsDemoUnmount() { const [unmount, setUnmount] = useState(false); const renderDemo = () => !unmount && <Counter />; return ( <div> <button onClick={() => setUnmount(true)}>Unmount child component</button> {renderDemo()} </div> ); }
This code implements a React component representing a counter that increases a number every second. The parent component renders the counter and allows you to destroy the counter by clicking on a button. Take a look at the recording to see what happens when a user clicks on that button:
The child component has registered an interval that invokes a function every second. However, the component was destroyed without unregistering the interval. After the component is destroyed, the interval is still active and wants to update the component’s state variable (count
), which no longer exists.
The solution is to unregister the interval right before unmounting. This is possible with a cleanup function. Therefore, you must return a callback function inside the effect’s callback body:
useEffect(() => { const interval = setInterval(function () { setCount((prev) => prev + 1); }, 1000); // return optional function for cleanup // in this case acts like componentWillUnmount return () => clearInterval(interval); }, []);
I want to emphasize that cleanup functions are not only invoked before destroying the React component. An effect’s cleanup function gets invoked every time right before the execution of the next scheduled effect.
Let’s take a closer look at our example. We used a trick to have an empty dependency array in the first place, so the cleanup function acts like a componentWillUnmount()
lifecycle method. If we do not call setCount
with a callback function that gets the previous value as an argument, we need to come up with the following code, wherein we add a count
to the dependencies array:
useEffect(() => { console.log("useEffect") const interval = setInterval(function () { setCount(count + 1); }, 1000); // return optional function for cleanup // in this case, this cleanup fn is called every time count changes return () => { console.log("cleanup"); clearInterval(interval); } }, [count]);
In comparison, the former example executes the cleanup function only once — on the mount — because we directly prevented using the state variable (count ):
useEffect(() => { console.log("useEffect") const interval = setInterval(function () { setCount(prev => prev + 1); }, 1000); // return optional function for cleanup // in this case, this cleanup fn is called every time count changes return () => { console.log("cleanup"); clearInterval(interval); } }, []);
In this context, the latter approach is a small performance optimization because we reduce the number of cleanup function calls.
Implications of prop and state changes
There is a natural correlation between prop changes and the execution of effects because they cause re-renders, and as we already know, effects are scheduled after every render cycle.
Consider the following example. The plan is that the Counter
component’s interval can be configured by a prop with the same name.
function Counter({ interval }) { const [count, setCount] = useState(0); useEffect(() => { const counterInterval = setInterval(function () { setCount((prev) => prev + 1); }, interval); return () => clearInterval(counterInterval); }, []); return <p>and the counter counts {count}</p>; } function EffectsDemoProps() { const [interval, setInterval] = useState(1000); return ( <div> <input type="text" value={interval} onChange={(evt) => setInterval(evt.target.value)} /> <Counter interval={interval} /> </div> ); }
The handy ESLint plugin points out that we are missing something important: because we haven’t added the interval
prop to the dependency array (having instead defined an empty array), the change to the input field in the parent component is without effect. The initial value of 1000
is used even after we adjust the input field’s value:
Instead, we have to add the prop to the dependency array:
useEffect(() => { const counterInterval = setInterval(function () { setCount((prev) => prev + 1); }, interval); return () => clearInterval(counterInterval); }, [interval]);
Now things look much better:
More on prop changes and using the useCallback
Hook
Let’s extend the example a bit to demonstrate more key concepts in conjunction with prop changes:
const Counter = ({ interval, onDarkModeChange }) => { console.log("render Counter"); const [count, setCount] = useState(0); useEffect(() => { console.log(`useEffect ${onDarkModeChange()}`); const counterInterval = setInterval(function () { setCount((prev) => prev + 1); }, interval); return () => clearInterval(counterInterval); }, [interval, onDarkModeChange]); return <p>and the counter counts {count}</p>; }; const IntervalConfig = ({ onDarkModeChange }) => { console.log("render IntervalConfig"); const [interval, setInterval] = useState(1000); const onChange = (evt) => setInterval(evt.target.value); return ( <div> <input type="text" value={interval} onChange={onChange} /> <Counter interval={interval} onDarkModeChange={onDarkModeChange} /> </div> ); }; const EffectsDemoProps = () => { console.log("render EffectsDemoProps"); const [numberClicks, setNumberClicks] = useState(0); const [darkMode, setDarkMode] = useState(false); const onDarkModeChange = () => (darkMode ? "🌙" : "🌞"); return ( <div style={ darkMode ? { backgroundColor: "black", color: "white" } : { backgroundColor: "white", color: "black" } } > <label htmlFor="darkMode">dark mode</label> <input name="darkMode" type="checkbox" checked={darkMode} onChange={() => setDarkMode((prev) => !prev)} /> <p> <button onClick={() => setNumberClicks((prev) => prev + 1)}> click </button> <span> Number clicks: {numberClicks}</span> </p> <IntervalConfig onDarkModeChange={onDarkModeChange} /> </div> ); };
I added log statements to indicate all component renderings and invocation of our useEffect
statement. Let’s take a look at what happens:
So far, so good — we can toggle the dark mode checkbox, and the effect should be executed, too. The callback function to be executed, onDarkModeChange
, is passed down the component tree to the Counter
component. We added it to the dependency array of the useEffect
statement as suggested by the ESLint plugin:
useEffect(() => { console.log(`useEffect ${onDarkModeChange()}`); const counterInterval = setInterval(function () { setCount((prev) => prev + 1); }, interval); return () => clearInterval(counterInterval); }, [interval, onDarkModeChange]);
As you can see from the recording, the effect is executed if one of the two props, interval
or onDarkModeChange
, changes.
All good? Not so fast — as you can see from the next recording, the effect is mistakenly executed if we click on the button:
Sure, the state of the EffectsDemoProps
changes, and this component is rendered along with its child components. The solution is to use React.memo
, right?
const Counter = React.memo(({ interval, onDarkModeChange }) => { // ... }); const IntervalConfig = React.memo(({ onDarkModeChange }) => { // ... });
The components are rendered, and the effect is still mistakenly executed:
Why is our Counter
component’s effect executed? The problem lies in the onDarkModeChange
function:
const EffectsDemoProps = () => { // ... const onDarkModeChange = () => (darkMode ? "🌙" : "🌞"); // ... };
On button click, the numberClicks
state of the EffectsDemoProps
component gets changed, and the component is thus re-rendered.
This is because onDarkModeChange
is defined inline of the component and gets recreated every time the component re-renders. So even if you use React.memo
on the child components, they get re-rendered because the passed onDarkModeChange
function prop points to another reference every time.
This is why it is crucial to understand the identity of values. In contrast to recreated primitive values like numbers, a recreated function points to another “cell” in memory. That’s why the function values differ.
We can fix this with the useCallback
Hook. In addition, we do not necessarily need to use React.memo
because it’s not really a problem to get the child components re-rendered in our example. However, we want to execute the effect only when the interval
value or the darkMode
value changes:
import React, { useState, useEffect, useCallback } from "react"; const Counter = ({ interval, onDarkModeChange }) => { // ... }; const IntervalConfig = ({ onDarkModeChange }) => { // ... }; const EffectsDemoProps = () => { // .. const onDarkModeChange = useCallback(() => { return darkMode ? "🌙" : "🌞"; }, [darkMode]); // ... };
With useCallback
, React only creates a new function whenever one of the dependencies changes — in our case, the darkMode
state variable. With this in place, our example works as expected:
useCallback
with useContext
Suppose we modify the example and use React Context with the useContext
Hook instead of passing down props to the child components. In that case, we still need to use useCallback
for the onDarkModeChange
dependency. The reasons are the same as in the previous section:
import React, { useState, useEffect, useCallback, useContext } from "react"; const EffectsContext = React.createContext(null); const Counter = ({ interval }) => { const [count, setCount] = useState(0); const { onDarkModeChange } = useContext(EffectsContext); useEffect(() => { const counterInterval = setInterval(function () { setCount((prev) => prev + 1); }, interval); return () => clearInterval(counterInterval); }, [interval, onDarkModeChange]); return <p>and the counter counts {count}</p>; }; const IntervalConfig = () => { const [interval, setInterval] = useState(1000); const onChange = (evt) => setInterval(evt.target.value); return ( <div> <input type="text" value={interval} onChange={onChange} /> <Counter interval={interval} /> </div> ); }; const EffectsDemoContext = () => { const [numberClicks, setNumberClicks] = useState(0); const [darkMode, setDarkMode] = useState(false); const onDarkModeChange = useCallback(() => { return darkMode ? "🌙" : "🌞"; }, [darkMode]); return ( <div style={ darkMode ? { backgroundColor: "black", color: "white" } : { backgroundColor: "white", color: "black" } } > <label htmlFor="darkMode">dark mode</label> <input name="darkMode" type="checkbox" checked={darkMode} onChange={() => setDarkMode((prev) => !prev)} /> <p> <button onClick={() => setNumberClicks((prev) => prev + 1)}> click </button> <span> Number clicks: {numberClicks}</span> </p> <EffectsContext.Provider value={{ onDarkModeChange }}> <IntervalConfig /> </EffectsContext.Provider> </div> ); };
useEffect
inside of custom Hooks
Custom Hooks are awesome because they lead to various benefits:
- Reusable code
- Smaller components because of outsourced code (effects)
- More semantic code due to the function calls of the custom Hooks inside of components
- Effects can be tested when used inside of custom Hooks, as we’ll see in the next section
The following example represents a custom Hook for fetching data. We moved the useEffect
code block into a function representing the custom Hook. Note that this is a rather simplified implementation that might not cover all your project’s requirements. You can find more production-ready custom fetch Hooks here:
const useFetch = (url, initialValue) => { const [data, setData] = useState(initialValue); const [loading, setLoading] = useState(true); useEffect(() => { const fetchData = async function () { try { setLoading(true); const response = await axios.get(url); if (response.status === 200) { setData(response.data); } } catch (error) { throw error; } finally { setLoading(false); } }; fetchData(); }, [url]); return { loading, data }; }; function EffectsDemoCustomHook() { const { loading, data } = useFetch( "https://jsonplaceholder.typicode.com/posts/" ); return ( <div className="App"> {loading && <div className="loader" />} {data?.length > 0 && data.map((blog) => <p key={blog.id}>{blog.title}</p>)} </div> ); }
The first statement within our React component, EffectsDemoCustomHook
, uses the custom Hook called useFetch
. As you can see, using a custom Hook like this is more semantic than using an effect directly inside the component.
Business logic is nicely abstracted out of the component. We have to use our custom Hook’s nice API that returns the state variables loading
and data
.
The effect inside of the custom Hook is dependent on the scope variable url
that is passed to the Hook as a prop. This is because we have to include it in the dependency array. So even though we don’t foresee the URL changing in this example, it’s still good practice to define it as a dependency. As mentioned above, there is a chance that the value will change at runtime in the future.
Additional thoughts on functions used inside of effects
If you take a closer look at the last example, we defined the function fetchData
inside the effect because we only use it there. This is a best practice for such a use case. If we define it outside the effect, we need to develop unnecessarily complex code:
const useFetch = (url, initialValue) => { const [data, setData] = useState(initialValue); const [loading, setLoading] = useState(true); const fetchData = useCallback(async () => { try { setLoading(true); const response = await axios.get(url); if (response.status === 200) { setData(response.data); } } catch (error) { throw error; } finally { setLoading(false); } }, [url]); useEffect(() => { fetchData(); }, [fetchData]); return { loading, data }; };
As you can see, we need to add fetchData
to the dependency array of our effect. In addition, we need to wrap the actual function body of fetchData
with useCallback
with its own dependency (url
) because the function gets recreated on every render.
By the way, if you move function definitions into effects, you produce more readable code because it is directly apparent which scope values the effect uses. The code is even more robust.
Furthermore, if you do not pass dependencies into the component as props or context, the ESLint plugin “sees” all relevant dependencies and can suggest forgotten values to be declared.
Using async functions inside of useEffect
If you recall our useEffect
block inside of the useFetch
custom Hook, you might ask why we need this extra fetchData
function definition. Can’t we refactor our code like so?
useEffect(async () => { try { setLoading(true); const response = await axios.get(url); if (response.status === 200) { setData(response.data); } } catch (error) { throw error; } finally { setLoading(false); } }, [url]);
I’m glad you asked, but no! The following error occurs:
The mighty ESLint plugin also warns you about it.
The reason is that this code returns a promise, but an effect can only return void or a cleanup function.
Unit testing of effects
Extracting useEffect
blocks into custom Hooks allows for unit testing them because you don’t have to deal with the actual React component. This is a significant benefit.
Some time ago, I wrote an article about unit testing custom Hooks with react-hooks-testing-library. This is one possibility to test the effects.
The following snippet is a Jest example that tests data fetching even with changing one of the effect’s dependencies (url
) during runtime:
import { renderHook } from "@testing-library/react-hooks"; import axios from "axios"; import MockAdapter from "axios-mock-adapter"; // import custom hook - in this example extracted to a separate file import useFetch from "./useFetch"; test("useFetch performs multiple GET requests for different URLs", async () => { // fetch 1 const initialValue = "initial value"; const mock = new MockAdapter(axios); const mockData = 1; const url = "http://mock"; mock.onGet(url).reply(200, mockData); const { result, waitForNextUpdate } = renderHook(() => useFetch(url, initialValue) ); expect(result.current.data).toEqual("initial value"); expect(result.current.loading).toBeTruthy(); await waitForNextUpdate(); expect(result.current.data).toEqual(1); expect(result.current.loading).toBeFalsy(); // fetch 2 const url2 = "http://mock2"; const mockData2 = 2; mock.onGet(url2).reply(200, mockData2); const initialValue2 = "initial value 2"; const { result: result2, waitForNextUpdate: waitForNextUpdate2 } = renderHook( () => useFetch(url2, initialValue2) ); expect(result2.current.data).toEqual("initial value 2"); expect(result2.current.loading).toBeTruthy(); await waitForNextUpdate2(); expect(result2.current.data).toEqual(2); expect(result2.current.loading).toBeFalsy(); });
useFetch
is wrapped in a renderHook
function call. This provides the correct context to execute the custom Hook without violating the rules of Hooks.
To perform the actual network call, we utilize waitForNextUpdate
. This allows us to wait for the asynchronous function to return to check the response from the network call. With this set, we can assert the result of our Hook. In our test, we mocked the actual network call with axios-mock-adapter.
You can also find this code in a CodeSandbox.
Some more useEffect
receipts
In this section, I’ll show you some handy patterns that might be useful.
Execute an effect only once when a certain condition is met
As we already know, you control the execution of effects mainly with the dependency array. Every time one of the dependencies has changed, the effect is executed. Mostly, you should design your components to execute effects whenever a state changes, not just once.
Sometimes, however, you want to do precisely this — e.g., when a certain event has occurred. You can do this with flags that you use within an if
statement inside of your effect. The useRef
Hook is a good choice if you don’t want to add an extra render (which would be problematic most of the time) when updating the flag. In addition, you do not have to add the ref to the dependency array.
The following example calls the function trackInfo
from our effect only if the following conditions are met:
- The user clicked the button at least once
- The user has ticked the checkbox to allow tracking
After the checkbox is ticked, the tracking function should only be executed after the user clicks once again on the button:
function EffectsDemoEffectConditional() { const [count, setCount] = useState(0); const [trackChecked, setTrackChecked] = useState(false); const shouldTrackRef = useRef(false); const infoTrackedRef = useRef(false); const trackInfo = (info) => console.log(info); useEffect(() => { console.log("useEffect"); if (shouldTrackRef.current && !infoTrackedRef.current) { trackInfo("user found the button component"); infoTrackedRef.current = true; } }, [count]); console.log("render"); const handleClick = () => setCount((prev) => prev + 1); const handleCheckboxChange = () => { setTrackChecked((prev) => { shouldTrackRef.current = !prev; return !prev; }); }; return ( <div> <p> <label htmlFor="tracking">Declaration of consent for tracking</label> <input name="tracking" type="checkbox" checked={trackChecked} onChange={handleCheckboxChange} /> </p> <p> <button onClick={handleClick}>click me</button> </p> <p>User clicked {count} times</p> </div> ); }
In this implementation, we utilized two refs: shouldTrackRef
and infoTrackedRef
. The latter is the “gate” to guarantee that the tracking function is only invoked once after the other conditions are met.
The effect is rerun every time count
changes, i.e., whenever the user clicks on the button. Our if
statement checks the conditions and executes the actual business logic only if it evaluates to true
:
The log message user found the button component
is only printed once after the right conditions are met.
Access data from previous renders
If you need to access some data from the previous render cycle, you can leverage a combination of useEffect
and useRef
:
function EffectsDemoEffectPrevData() { const [count, setCount] = useState(0); const prevCountRef = useRef(); useEffect(() => { console.log("useEffect", `state ${count}`, `ref ${prevCountRef.current}`); prevCountRef.current = count; }, [count]); const handleClick = () => setCount((prev) => prev + 1); console.log("render"); return ( <div> <p> <button onClick={handleClick}>click me</button> </p> <p> User clicked {count} times; previous value was {prevCountRef.current} </p> </div> ); }
We synchronize our effect with the state variable count
so that it is executed after the user clicks on the button. Inside of our effect, we assign the current value of the state variable to the mutable current
property of prevCountRef
. We output both values in the JSX section:
On loading this demo, on initial render, the state variable has the initial value of the useState
call. The ref value is undefined
. It demonstrates once more that effects are run after render. When the user clicks, it works as expected.
When not to use useEffect
There are some situations in which you should avoid using useEffect
due to potential performance concerns.
1. Transforming data for rendering
If you need to transform data before rendering, then you don’t need useEffect
. Suppose you are showing a user list and only want to filter the user list based on some criteria. Maybe you only want to show the list of active users:
export const UserList = ({users}: IUserProps) => { // the following part is completely unnecessary. const [filteredUsers , setFilteredUsers] = useState([]) useEffect(() => { const activeUsers = users.filter(user => user.active) setFilteredUsers(activeUsers) ,[users]) return <div> {filteredUsers.map(user => <div> {user.name} </div>)} </div> }
Here you can just do the filtering and show the users directly, like so:
export const UserList = ({users}: IUserProps) => { const filteredUsers = users.filter(user => user.active) return <div> {filteredUsers.map(user => <div> {user.name} </div>)} </div> }
This will save you time and improve the performance of your application.
2. Handling user events
You don’t need useEffect
for handling user events. Let’s say you want to make a POST request once a user clicks on a form submit button. The following piece of code is inspired from React’s documentation:
function Form() { // Avoid: Event-specific logic inside an Effect const [jsonToSubmit, setJsonToSubmit] = useState(null); useEffect(() => { if (jsonToSubmit !== null) { post('/api/register', jsonToSubmit); } }, [jsonToSubmit]); function handleSubmit(e) { e.preventDefault(); setJsonToSubmit({ firstName, lastName }); } }
In the above code, you can just make the post request once the button is clicked. But you are cascading the effect, so once the useEffect
is triggered, it doesn’t have the complete context of what happened. This might cause issues in the future; instead, you can just make the POST request on the handleSubmit
function:
function Form() { function handleSubmit(e) { e.preventDefault(); const jsonToSubmit = { firstName, lastName }; post('/api/register', jsonToSubmit); } }
This is much cleaner and can help reduce future bugs.
Conclusion
Understanding the underlying design concepts and best practices of the useEffect
Hook is a key skill to master if you wish to become a next-level React developer.
If you started your React journey before early 2019, you have to unlearn your instinct to think in lifecycle methods instead of thinking in effects.
Adopting the mental model of effects will familiarize you with the component lifecycle, data flow, other Hooks (useState
, useRef
, useContext
, useCallback
, etc.), and even other optimizations like React.memo
.
LogRocket: Full visibility into your production React apps
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time,
try LogRocket.
LogRocket
combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?
Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.
No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps —
start monitoring for free.
Реагируйте подробно
Вступление
Недавно я написал статью о Контекст против состояния против Redux. Цель состояла в том, чтобы попытаться дать разработчикам некоторые плюсы и минусы каждого варианта и, надеюсь, помочь вам сделать лучший выбор в отношении инструмента, который вы планируете использовать.
После того, как я его выпустил, несколько человек задали мне вопросы о миграции на хуки в отношении использования Redux. У некоторых разработчиков возникали проблемы с useEffect
запуском на каждом рендере при использовании mapDispatchToProps
с connect
HOC React Redux.
Это были отличные вопросы! Подумав немного, давайте разберемся, почему вы это видите, и, надеюсь, поможем вам понять, почему это происходит.
Итак, шаги, которые мы собираемся выполнить в этой статье, чтобы сначала объяснить предысторию, а затем решить эту проблему:
- Что такое
useEffect
крючок? - Что такое массив зависимостей?
- Работают ли функции в массиве зависимостей?
- Можем ли мы использовать
mapStateToDispatch
сuseEffect
?
А как насчет useDispatch?
Прежде чем мы начнем, я должен сразу ответить; есть useDispatch
Hook, предоставленный React Redux.
Этот крючок позволяет нам отправлять сообщения так же, как и в предыдущем connect
HOC, и я настоятельно рекомендую использовать его в будущем.
Мы рассмотрим некоторые подробности того, как это работает с useEffect
позже в этой статье, но вместо этого мы сосредоточимся на проблемах, связанных с работой над более «устаревшей» (если мы можем ее так назвать) системой, которая все еще использует connect
.
Что такое useEffect?
Проще говоря, useEffect
— это ловушка, которая позволяет выполнять побочные эффекты в функциональных компонентах. Для большей детализации эти эффекты выполняются только после рендеринга компонента, поэтому не блокируют сам рендеринг.
Часто это будет что-то вроде запроса на выборку некоторых данных, которые требуются компоненту. Если бы мы не обрабатывали побочные эффекты, как бы отрисовывался компонент React? Могут ли наши побочные эффекты блокировать пользовательский интерфейс?
Блокировка UI
Одна из первых проблем, с которыми вы можете столкнуться с эффектами непосредственно в коде, заключается в том, что вы можете очень легко заблокировать пользовательский интерфейс от рендеринга. Вы не сможете заблокировать его запросом. React помешает вам получить эту возможность.
Например, возможно, на первый взгляд, вы решите, что лучше подождать, пока эти данные не будут возвращены, прежде чем что-либо визуализировать. React выдаст ошибку, если вы попытаетесь использовать асинхронные функции.
Примечание. В React появятся прерываемые компоненты! Это будет в параллельном режиме, и я настоятельно рекомендую прочитать об этом подробнее. Возможно, в одной из моих будущих статей можно будет изучить код!
Мне стало любопытно, можно ли заставить React ждать моего запроса, но я не смог. Я даже выбрал отвратительный метод sleep
, и JavaScript правильно завершит функцию, прежде чем она вернется для выполнения обещания. Я просто подумал, что это неплохо.
Но приведенный выше пример действительно иллюстрирует блокирующий пользовательский интерфейс. Обратите внимание, что когда один компонент заблокирован, он останавливает рендеринг страницы, ожидая ее завершения.
Вот почему мы не хотим блокировать какой-либо рендеринг от наших побочных эффектов, и это должно быть достаточной причиной, чтобы попытаться найти способ изолировать эту функцию до завершения первоначального рендеринга. Даже с учетом этого, давайте посмотрим на примеры, не связанные с блокировкой
Предположим, мы не блокируем пользовательский интерфейс, но по-прежнему используем обещания. Наш код может выглядеть примерно так:
Здесь есть пара вопросов, давайте рассмотрим их по очереди.
Бесконечный повторный рендеринг
Очевидная проблема с приведенным выше кодом заключается в том, что это создаст бесконечный цикл повторного рендеринга. Это также может произойти в useEffect
, но есть другие правила, чтобы этого не происходило.
Что, если вместо этого мы попытаемся наблюдать, когда в состоянии есть данные, что-то вроде этого:
Если мы сделаем что-то подобное, это будет выглядеть намного лучше. Я имею в виду, что страница существует, а это всегда на шаг впереди. Проблемы с этим подходом становятся более очевидными, когда мы пытаемся рассуждать об этом компоненте. Затем, оттуда, по мере того, как он становится сложнее.
Итак, давайте спросим себя, что это за данные? Для самой функции это никогда не «сейчас». Это всегда данные предыдущего рендера.
Компонентное рассуждение
В приведенном выше примере обоснование компонентов не является большой проблемой. Нам нужны эти данные только один раз, и мы не беспокоимся о загрузке и т. Д.
Но что, если бы пользователь мог передать URL-адрес, это означает, что его действительно нужно вызывать, когда у него есть данные, но только данные из указанного URL-адреса. Эти данные взяты из предыдущего рендера, а не из этого рендера.
Теперь, конечно, эффект useEffect
выполняется после рендеринга, так что вы можете утверждать, что с точки зрения логистики он не сильно отличается от того, что было раньше.
Но я бы сказал, что, помещая что-то в useEffect
, вы конкретно говорите следующему разработчику, который увидит этот код, что логически он не входит в поток этого компонента.
Он не предназначен для чтения сверху вниз. Эту функцию можно представить как после того, как остальная часть этого компонента будет выполнена. Принимая во внимание, что когда что-то встроено в функциональный компонент, предполагается, что данные будут доступны для компонента сейчас.
А что, если для сбора необходимых данных потребовалось время? Выполнение длительного кода может заблокировать рендеринг пользовательского интерфейса, но для простоты я просто воспользуюсь тайм-аутом, чтобы подделать это.
Если вы визуализируете это, вы можете заметить, что компонент обновляется несколько раз и запрашивает факты непредсказуемое количество раз. Это потому, что в нашем рендеринге есть некоторые крайние случаи, о которых мы теперь должны помнить.
Что произойдет, если кто-то обновит это при отсутствии данных, но мы уже собираем данные для получения? Что произойдет, если выборка будет запрошена, но наш компонент обновит и запросит другой?
Представьте, что у нас также были взаимодействия, которые могли бы обновить элемент за это время, что не кажется надуманным. Становится все труднее и труднее рассуждать о точном состоянии нашего компонента и о том, что происходит в данный момент.
Нельзя сказать, что подобные вещи невозможны. Но должен быть способ получше, который подводит нас к одному заключительному пункту.
Функциональная чистота
Если вы работали в мире функционального программирования, подобные эффекты в ваших функциях не одобряются. Вы хотите убедиться, что компонент имеет ссылочную прозрачность. Побочные эффекты снижают чистоту вашей функции.
Я считаю, что функциональное программирование — это отличный актив, но сегодня я не хочу заходить в него слишком далеко. Самый простой способ взглянуть на это — спросить себя, учитывая, что вы передаете компоненту определенный набор входных данных, можете ли вы ожидать такого же результата?
Каждый побочный эффект, который есть в нашем коде, снижает вероятность того, что это правда. Теперь, прежде чем вы попытаетесь удалить абсолютно все побочные эффекты из вашей системы, просто имейте в виду, что пользовательский ввод сам по себе является побочным эффектом, а использование useEffect
все еще является побочным эффектом.
Вместо этого нашей целью должно быть управление, изоляция и контроль. Подобная обработка входных данных также удобна в будущем для тестирования, если мы хотим изолировать тестирование внутри компонента или системы.
Чтобы эта функция была более чистой, мы будем использовать массивы зависимостей. Позже мы подробно рассмотрим массивы зависимостей, но давайте посмотрим, как это выглядит сейчас.
Красота использования useEffect
Hook также заключается в сочетании его с правилами ESLint, которые разработала команда React, которые позволят вам узнать зависимости, предлагаемые в массиве, и помогут сохранить эту функцию как можно более чистой.
Как этот пример выглядит как useEffect?
Примечание. В приведенном выше примере fetch
также должен быть частью массива зависимостей useEffect
. Я не хотел добавлять слишком много шума, пока мы работаем над примером, но мы вернемся к тому, почему он должен быть там, позже в статье.
Глядя на useEffect
, он не сильно отличается от наших предыдущих примеров. Вы заметите, что если вы снова добавите тайм-аут в пример кода, он и сейчас будет работать правильно.
Позже мы разберемся, почему это так, но заметим, что на самом деле мы добавили только две новые части: вызов useEffect
и массив зависимостей [id, setData]
.
Как я уже упоминал ранее, по мере знакомства с хуками я обнаружил, что useEffect
говорит мне, что этот фрагмент кода не повлияет на компонент до тех пор, пока не будет выполнена первоначальная визуализация.
Это позволяет мне быстро находить «побочные эффекты» в нашем коде, уменьшая когнитивную нагрузку при прохождении через компонент и повышая удобочитаемость.
Если это запрос, который мы довольно часто видим в других компонентах, мы могли бы даже разбить его на собственный пользовательский Hook, который может еще больше помочь с удобочитаемостью:
Итак, я должен использовать useEffect Hook для устранения побочных эффектов?
Уф, да. Вам следует. Это был длинный список причин, по которым вы не должны размещать его внутри компонента, но я надеюсь, что эти примеры помогут вам понять, почему это было создано.
useEffect
позволяет:
- Не блокировать пользовательский интерфейс.
- Создайте визуальный «блок» кода, который представляет собой видимый эффект.
- Держите свои функции в чистоте (или, как он пытается).
- Повысьте читаемость вашего кода
- Легко извлекайте, когда это необходимо, в пользовательские хуки, чтобы мы могли делиться ими между другими компонентами.
Как это связано с компонентом класса?
Первый вопрос, который часто возникает, когда я начинаю обсуждать хуки, у давних разработчиков React: «Что такое хуки метода жизненного цикла?»
Или, когда показываю их useEffect
, меня спрашивают: «Это где мой componentDidMount
?» И я очень осторожно отвечаю на эти вопросы.
По сути, людям нужен способ логически сопоставить компонент класса с функциональным компонентом 1: 1, но я не решаюсь связать его таким образом. Вам придется отказаться от мышления в сопоставлении 1: 1 между функциональными компонентами и компонентами класса.
Попытайтесь переосмыслить функциональный компонент по-новому, с наиболее важным ключевым моментом в том, что эта функция может и должна быть вызываемой в любое время.
Что-то называется не только «на гору». Вместо этого их следует вызывать с массивом зависимостей, например с родительским состоянием. И только если эта зависимость изменится, это следует вызывать снова.
Массивы зависимостей также работают вне useEffect
, useCallback
и useMemo
— другие отличные примеры.
Мы хотим запомнить функцию или возвращаемое значение и вызывать / повторно инициализировать эти вещи только в случае изменения конкретных данных в массиве зависимостей. Но это должно быть вызвано в любое время.
Примечание. Если вы хотите получить более подробное описание этого, я настоятельно рекомендую ознакомиться с анализом useEffect
Дэна Абрамова.
Что такое массив зависимостей?
Итак, я несколько раз выбрасывал «массив зависимостей», лучше всего разбить его и понять, что он делает. Массив зависимостей — второй необязательный аргумент в функции useEffect
.
Как следует из названия, это массив зависимостей, который при изменении по сравнению с предыдущей визуализацией будет вызывать функцию эффекта, определенную в первом аргументе. Давайте сначала посмотрим на пример:
Итак, что именно делает этот эффект? Что ж, каждый раз, когда этот компонент обновляется, он будет вызывать это useEffect
. Возможно, мы этого хотим, и это приемлемо.
Но чаще всего вы, вероятно, хотите, чтобы этот эффект произошел только из-за чего-то другого. Возможно, только при изменении данных или, может быть, когда пользователь впервые видит компонент.
В приведенном выше примере кажется вероятным, что мы хотим, чтобы этот эффект запускался только при изменении значения в id
. Для этого мы указываем эффекту «запускаться только при id
изменениях», помещая id
в массив зависимостей.
Теперь этот эффект выполняется, только если id
отличается от предыдущего обновления. Я упоминал выше, что, возможно, вы хотите, чтобы он запускался только тогда, когда пользователь «впервые видит компонент», но я также упомянул ранее, чтобы не думать о нем как о componentDidMount
… Разве я не противоречу себе?
Не совсем так, потому что, когда мы говорим о том, что «впервые увидел», мы на самом деле говорим об этом с точки зрения пользователя. Их опыт не обязательно должен соответствовать коду, как мы всегда ожидаем.
Скорее всего, если мы загружаем данные для страницы, цель состоит в том, чтобы получить эти данные, когда пользователь попадает на эту страницу. Итак, если вы хотите это сделать, вы можете сделать что-то вроде этого:
Это будет получать данные только при первом вызове. И в этом есть смысл, правда? Мы дали эффекту массив зависимостей. Итак, наш эффект думает: «Эй, вспоминайте этот эффект только при изменении значений в этом массиве». Но нет значений, поэтому он никогда не может обновиться.
Но я предостерегаю вас от создания ваших эффектов с использованием этого стиля. И команда React тоже.
Это может привести к сбивающим с толку ошибкам, которые трудно найти. Вместо этого давайте на мгновение задумаемся об этом компоненте. Мы передаем URL-адрес, который определяет, где мы выполняем эту выборку. Что произойдет, если мы поместим URL в такой массив?
Есть один из трех исходов.
- Этот URL-адрес никогда не изменяется, и эффект должен выполняться только один раз.
- Этот URL-адрес меняется, но вы хотели, чтобы он запрашивал только в первый раз.
- Этот URL-адрес меняется, и вы хотите, чтобы он запрашивал каждый раз, когда это происходит.
Для номеров 1 и 3 вам подходит второе решение, независимо от того, каким путем вы хотите идти.
Для номера 1, если URL-адрес никогда не меняется при входе в опору после первого раза, значит, он уже работает правильно. URL-адрес — это зависимость, и эта зависимость не предназначена для изменения.
Если по какой-то причине она изменится, вы увидите эту ошибку намного быстрее, тогда как передача пустого массива в массив зависимостей «спрячет» эту ошибку от вас.
Для числа 3 это то, для чего построен массив зависимостей. Каждый раз, когда этот URL изменяется, эффект будет запускаться повторно, и вам не нужно настраивать какую-либо сложную логику, чтобы привязать его к компоненту, как мы должны были сделать в начале статьи.
Итак, единственная реальная проблема — это номер 2. Но если вы остановитесь и задумаетесь, кажется, что есть более серьезная проблема в том, как вы конструируете свои компоненты в этом примере.
Вы хотите, чтобы данные поступали, но эти данные представляют только один экземпляр вашего компонента. Если бы кто-то позже сделал снимок вашего компонента и передал эти входные данные новому компоненту, вы бы не получили тот же результат.
В нашем примере отсутствует ссылочная прозрачность, которую мы хотим от наших компонентов. Мы должны ожидать, что при заданном наборе входных данных мы всегда получим один и тот же результат.
И наша цель должна заключаться в том, чтобы эти компоненты можно было вызывать в любое время. Итак, я настоятельно рекомендую вам, если вы достигли результата номер 2, переосмыслить свой компонент.
Работают ли функции в массиве зависимостей?
Да! Они работают так же, как значения в массиве зависимостей, но вместо этого привязаны к ссылке на эту функцию. Ожидается, что каждый раз, когда ссылка на эту функцию изменяется, мы повторно запускаем этот эффект.
В нашем примере выше мы жульничали, потому что никогда не определили, что такое fetchData
. Давайте рассмотрим случай, когда это функция без нашего компонента.
Так это работает, правда? Что ж, да, но, вероятно, не так, как мы этого хотим, поскольку теперь это снова будет вызывать наш эффект при каждом обновлении компонента. Каждый раз, когда ExampleComponent
обновляется, мы собираемся повторно инициализировать функцию fetchData
.
Примечание. Опять же, мы не хотим связывать нашу ментальную модель с компонентами класса, и многие люди привыкли к экземплярам переменных / методов в компонентах класса.
Что касается функциональных компонентов, у нас нет «предыдущих значений», которые существовали бы во всех обновлениях. Если функция вызывается снова, все эти значения создаются снова, включая функции.
Ваша первая мысль о проблеме, описанной выше, может быть такой: «А зачем вообще функция в массиве зависимостей? Если я его сниму, все будет нормально ».
Да, в этом случае он будет работать так, как вы ожидали, но если вы используете React ESLint for Hooks (а я настоятельно рекомендую вам это сделать), вы заметите, что это не так.
И это по той же причине, о которой мы говорили выше, с при первом рендеринге и нашей url
опорой. Это может дать желаемый результат, но также может скрыть нежелательную ошибку.
В приведенном выше примере мы действительно ожидаем, что когда-либо будет использоваться только одна функция, но вы можете передать функцию как опору своему компоненту. И эта функция может быть одной из нескольких возможных функций.
Как и в случае с url
, мы хотим убедиться, что при получении новой функции мы снова вызываем эффект.
Итак, без рассуждений, как мы можем решить нашу проблему с ESLint и сохранить наш эффект как можно более чистым. Что ж, есть три подхода, и каждый решает разные варианты использования.
Подход 1. Переместите функцию в эффект
Пример использования: этот fetchData
вызов всегда используется только в этом локальном useEffect
.
Если вы планируете когда-либо использовать эту функцию только в этом единственном useEffect
, наиболее простое и предлагаемое решение — переместить функцию непосредственно в закрытие эффекта.
Это работает для всего, что мы обсуждали ранее, и гарантирует, что сама наша функция эффекта будет максимально чистой и ссылочно прозрачной. Он инкапсулирует логику в одну область, а также позволяет разработчикам узнать, что эта функция предназначена как побочный эффект.
Итак, как это выглядит?
Поскольку функция fetchData
теперь является частью нашего эффекта, она больше не является зависимостью от нашего эффекта, и мы можем просто удалить ее из массива зависимостей.
Подход 2. Запомните функцию с помощью useCallback
Пример использования: эта функция используется в нескольких локальных хуках или будет передана в дочерний компонент.
useCallback
— один из новых хуков, доступных для React. Это позволяет нам запоминать функцию, чтобы при последующих обновлениях компонента функция сохраняла ссылочное равенство и, следовательно, не запускала эффект.
useCallbacks
использует тот же массив зависимостей, что и useEffect
, поэтому, если значения или функции, от которых он зависит, изменятся, он будет повторно инициализирован. Чтобы понять, как это работает, я думаю, было бы полезно перейти к ссылочному и ценностному равенству.
Равенство ссылок и равенство ценностей
Для наших целей мы рассматриваем два типа равенства: значение и ссылочное значение (также известное как составные значения).
Равенство ценностей немного легче понять. Равенство значений — это сравнение фактического «значения» переменной, например:
В приведенном выше примере A === C
, а B
тоже не равно. Но если мы изменим A
сейчас, значения A
и C
больше не будут равны.
Теперь эти значения не только больше не равны, но и указывают на два разных места в памяти. Мы знаем это, потому что, когда мы меняем A, C не меняется вместе с ним.
Если бы они находились в одном месте в памяти, оба значения обновлялись бы одновременно. Вот почему они равны по значению, но не равны ссылочно. Давайте посмотрим на диаграмму того, как это выглядит с нашей памятью:
Для примитивных значений, таких как строка или целое число, мы можем сравнивать только по значению, у нас нет способа сохранить ссылку на другую переменную. Даже если мы сделаем что-то вроде этого:
Несмотря на то, что мы указываем переменную B
на нашу A
переменную, мы не получаем места в памяти, мы все равно просто получаем копию значения переменной.
Многие другие языки действительно имеют возможность хранить «ссылку» на переменную, часто называемую указателем. Вероятно, это наиболее известно в языках C / C ++.
С другой стороны, мы можем сравнивать ссылочные значения с составными значениями, такими как объекты, функции и массивы.
Примечание. Технически функции и массивы также являются объектами в JavaScript.
Чтобы вернуться к использованию функций в массиве зависимостей useEffect
, давайте взглянем на ссылочное равенство функции:
Обе функции A и B указывают на одно и то же место в памяти. Теперь, если бы кто-то переназначил A, он переместил бы A на новое место в памяти, как указано выше, а B по-прежнему указывал бы на исходное место.
С другой стороны, если A содержал объект, и мы изменили этот объект, B также получит эти обновления. Давайте посмотрим на приведенный выше пример кода на диаграмме.
Вернемся к нашей памятной функции
Итак, зная, что мы можем сохранить ссылку на место в памяти для данной функции, мы можем передать эту ссылку в массив зависимостей useEffect
.
Если компонент повторно отрисовывается, и эта функция не указывает на то же место в памяти (даже если это та же функция и параметры), useEffect
будет вызываться снова, потому что он видит его как новую функцию.
Если мы можем запомнить (запомнить) ссылку на функцию, это означает, что мы можем остановить повторный запуск useEffect
, если он действительно не изменился. Посмотрим, как это выглядит.
Если вы собираетесь использовать функцию только один раз, я рекомендую переместить логику, как указано выше, но этот шаблон удобен, когда вам нужно передать функцию нескольким useEffects
.
Вы обнаружите, что useCallback
еще удобнее передавать функции дочерним компонентам. Если мы не будем использовать этот шаблон, дочерний компонент будет повторно отображать каждое обновление, даже если оно запомнено. Это потому, что функция никогда не будет иметь такого же ссылочного равенства с предыдущей визуализацией.
Более того, если у этого дочернего компонента есть какие-либо хуки, зависящие от этой функции, они будут вызываться каждый раз. По этой причине всегда полезно создавать функции, которые передаются дочерним компонентам с помощью useCallback
.
Примечание. Это не означает, что вы должны создавать каждую функцию с useCallback
. Это важно только в том случае, если он передается дочерним компонентам. Запоминание вызовов локальных функций часто может добавить ненужные накладные расходы и сложность к вашему коду.
Итак, мы знаем, что если мы используем наши функции в массиве зависимостей, мы должны запоминать их, заключив это в useEffect
. Если этого не сделать, эффект будет повторяться после каждого обновления компонента.
Итак, почему в исходном примере useEffect
я сделал это с функцией setData
?
Это потому, что функция, возвращаемая в useState
Hook, уже запомнена для вас. То же самое и с useReducer
. И это устанавливает фундаментальный принцип дизайна для вас, как разработчика, движущегося вперед по мере создания ваших хуков.
Если вы возвращаете функцию из своего хука, весьма вероятно, что вы хотите, чтобы эта функция была запомнена, чтобы разработчики могли использовать их без дополнительных накладных расходов на их обработку.
Рассмотрение примера
Я помог другу пройтись по примеру кода, чтобы лучше понять, как мемоизированные функции работают по сравнению с немомоизованными функциями, взяв пример кода, а затем нарисовав вместе с ним диаграмму.
Я думаю, что это выглядит немного запутанным, если я не буду обсуждать это в устной форме в то же время, поэтому я хотел бы очистить его и, возможно, оживить его, но до тех пор я опубликую его здесь, если он будет кому-то полезен.
Пример кода можно найти в CodeSandbox. Код ниже — это то, через что мы собираемся пройти; функция memoized
использует метод useCallback
, который мы описали выше, в то время как функция notMemoized
будет повторно инициализироваться при каждом обновлении.
Затем обе функции используются одним и тем же компонентом (разные экземпляры), и есть две визуализации примера, чтобы дать представление о том, что происходит во время обновления.
Подход 3. Вместо этого импортируйте функцию
Последний стиль, который мы можем использовать для нашей fetchData
функции, фактически перемещает ее за пределы самого компонента.
Об этом стиле не так часто говорят, как о предыдущих двух, но, в зависимости от того, что он делает, он часто может быть моим любимым. Этот стиль действительно требует, чтобы вы использовали стиль ESM import
в ваших модулях, а не стиль CJS.
Примечание. Не вдаваясь в подробности, это связано с тем, что операторы импорта дадут нам единственный экземпляр функции, которая не может быть изменена, тогда как экспортированный модуль с CJS может быть изменен.
По этой же причине ESM поддается статическому анализу. Если вы хотите узнать больше об этом, модули довольно часто упоминаются в серии Уменьшение размера пакета JS.
В приведенном выше примере это может выглядеть примерно так:
Поскольку модуль не может быть изменен, нам не нужно указывать функцию в нашем массиве зависимостей, поскольку она не может быть изменена. Теперь вы все еще можете поместить функцию в массив зависимостей, но даже ESLint не заставит вас это сделать.
Итак, почему это лучше? Просто чтобы не помещать функцию в массив зависимостей?
Самая большая причина, по которой я часто разделяю свои функции на такие функции, как это, заключается в том, чтобы повысить их тестируемость. Иногда в моем useEffect
есть сложная логика, которую я хотел бы протестировать индивидуально.
Теперь есть способы протестировать хуки, но это сделать намного сложнее. Я также обнаружил, что разделение этих фрагментов кода на именованные функции увеличивает читаемость моего кода, поэтому часто бывает более полезно сделать это уже, а перенос его в новый модуль дает мне лучшее бесплатное тестирование.
Наконец, когда мы это делаем, это также упрощает имитацию побочных эффектов этого эффекта, когда мы проводим интеграционное тестирование этого компонента.
В этом примере мы выделили фактический побочный эффект асинхронной выборки. Мы могли бы имитировать fetchData
метод и вместо этого просто вызвать аргумент setData
, который будет заполнен данными, которые мы хотим проверить.
Можно ли использовать функции диспетчеризации Connect в массиве зависимостей?
Итак, теперь, когда вся эта справочная информация убрана, можем ли мы наконец ответить на исходный вопрос? Что, если мы будем использовать mapDispatchToProps
с нашим useEffect
, он все равно будет работать? Будет ли он отрендерен повторно или нам придется оборачивать каждую опору в useCallback
?
Нет, вам не нужно ничего делать с mapDispatchToProps
, он будет работать прямо из коробки, как мы и предполагали, точно так же, как useDispatch
. Даже прошлые версии. Но почему так?
Что ж, React Redux запомнил свои функции как в старом коде, так и в новом useDispatch
Hook, который был недавно выпущен. Они следовали принципам дизайна, о которых мы говорили выше, и позаботились о том, чтобы вам не пришлось запоминать их, когда вы их вернете.
Но это также дает значительные преимущества в производительности для предыдущего mapDispatchToProps
кода. Если бы библиотека React Redux не справлялась с этим, пользователям приходилось бы обрабатывать эту разницу в ссылочном равенстве каждый раз, когда их компонент обновлялся.
Но тогда почему у некоторых пользователей возникали проблемы при использовании старого примера mapDispatchToProps
?
Ну, мне потребовалось немного времени, чтобы понять, почему, но все сводится к ownProps
. ownProps
— это второй аргумент, который необязательно передается в вызов функции mapDispatchToProps
и представляет собой реквизиты, поступающие от родительского компонента.
Подробнее об этой проблеме читайте подробнее здесь.
Итак, в этом примере:
Если бы мы заглянули внутрь параметра ownProps
, который является частью нашего mapDispatchToProps
, мы бы увидели объект, который выглядел бы так:
Теперь наш пример выше полностью надуманный, поскольку на самом деле ничего не происходит.
Тем не менее, пока у нас есть свойство, передаваемое в Container
компонент, и у нас есть ownProps
аргументы в нашем mapDispatchToProps
, эта функция будет терять свое ссылочное равенство при каждом обновлении.
И это имеет смысл, поскольку этот объект будет новым объектом при каждом рендеринге. Нам пришлось бы добавить дополнительную логику для обработки проверок равенства, чтобы определить, совпадают ли данные в объекте.
Мне было легче думать об этой проблеме с точки зрения useCallback
. Если useCallback
был частью нашего компонента, мы фактически запускаем аналогичную проблему.
Передача всего объекта prop — это не тот способ, которым вы обычно создаете useCallback
, а не отдельные значения, которые вы ищете. Но у него есть аналогичная проблема, когда вы отслеживаете весь объект, созданный при обновлении, а не необходимое значение.
Всегда легче обеспечить равенство значений, таких как строки и целые числа, чем путем сравнения ссылок на память или глубоких сравнений, как это можно было бы сделать с объектами, массивами или функциями.
Итак, я бы сказал, что решить эту проблему в useCallback
s сложнее, потому что это более неестественный способ структурировать код, что и делает хуки такими замечательными, когда вы лучше с ними знакомитесь. Часто стиль кода подталкивает вас к формату, который менее подвержен ошибкам.
В случае вышеупомянутого я все же предлагаю вам использовать useDispatch
, если это вообще возможно, чтобы избежать подобных проблем, но если вы не можете этого сделать, просто имейте в виду, что, используя ownProps
, вы, вероятно, сделаете свой код менее производительным. и более подвержены ошибкам.
Если вы хотите продолжать использовать mapDispatchToProps
, лучший подход — передать необходимые значения через функцию, а затем захватить через ownProps
.
Заключение
Надеюсь, вы нашли сегодняшнее пошаговое руководство полезным и лучше понимаете, что такое useEffect
, и, что наиболее важно, как работает массив зависимостей. Эти знания работают с несколькими хуками и являются фундаментальными для построения ментальной модели функциональных компонентов.
Самая важная часть, от которой вы уйдете, — это более четкое понимание того, как эти значения и ссылки должны обрабатываться в функциональном компоненте.
Цель состоит в том, чтобы всегда позволять вашему компоненту быть визуализируемым и иметь максимально возможную ссылочную целостность.
Одна из наиболее распространенных проблем, с которыми я сталкивался с людьми, плохо знакомыми с хуками и функциональными компонентами, — это неправильное понимание зависимостей и ссылок на функции, которое вызывает слишком большое количество повторных отрисовок или нежелательных побочных эффектов.
Я знаю, что многие люди стали немного опасаться хуков в целом, особенно переходя на новые хуки React Redux, но я лично обнаружил, что они делают мой код более читабельным и менее подверженным ошибкам, когда вы становитесь старше. привычный.
Я настоятельно рекомендую плагин ESLint, выпущенный командой React, так как они помогут вам делать правильные вызовы, когда вы больше привыкните к стилю.
Я пытаюсь вводить больше наглядных диаграмм в свои рецензии и, надеюсь, анимировать их все больше и больше. Я ни в коем случае не иллюстратор, но по моему личному опыту, я часто нахожу, что визуальные очереди намного легче запомнить по сравнению с большими рецензиями.
Я также подумал о том, чтобы дополнить эти статьи видео-объяснением / кодированием для лучшего руководства, поэтому дайте мне знать, если это то, что вас заинтересует!
Если у вас есть какие-либо другие идеи, которые помогут сделать эти темы более информативными, или темы, о которых вы хотели бы услышать, пожалуйста, оставьте комментарий.
Ваше здоровье!