Полное руководство по useeffect

Вы написали несколько компонентов с использованием хуков. Возможно — даже создали небольшое приложение. В целом результат вас вполне устраивает. Вы привыкли к 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 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.

  • optional dependencies: The list of all reactive values referenced inside of the setup 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 the Object.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 generally 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 with useLayoutEffect.

  • 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 with useLayoutEffect.

  • 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:

  1. 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.
  2. 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:

  1. Your setup code runs when your component is added to the page (mounts).
  2. 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.
  3. 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 setupcleanupsetup 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 that it 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.

useEffect — это хук React, который позволяет вам синхронизировать компонент с внешней системой.

useEffect(setup, dependencies?)

Описание¶

useEffect(setup, dependencies?)

Вызовите useEffect на верхнем уровне вашего компонента, чтобы объявить Эффект:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
    const [serverUrl, setServerUrl] = useState(
        'https://localhost:1234'
    );

    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        );
        connection.connect();
        return () => {
            connection.disconnect();
        };
    }, [serverUrl, roomId]);
    // ...
}

Параметры¶

  • setup: Функция с логикой вашего Эффекта. Ваша функция настройки может также по желанию возвращать функцию cleanup. Когда ваш компонент будет добавлен в DOM, React запустит вашу функцию настройки. После каждого повторного рендеринга с измененными зависимостями React будет сначала запускать функцию очистки (если вы ее предоставили) со старыми значениями, а затем запускать вашу функцию настройки с новыми значениями. После удаления вашего компонента из DOM React запустит вашу функцию очистки.
  • опциональные dependencies: Список всех реактивных значений, на которые ссылается код setup. Реактивные значения включают props, state, а также все переменные и функции, объявленные непосредственно в теле вашего компонента. Если ваш линтер настроен на React, он проверит, что каждое реактивное значение правильно указано в качестве зависимости. Список зависимостей должен иметь постоянное количество элементов и быть написан inline по типу [dep1, dep2, dep3]. React будет сравнивать каждую зависимость с предыдущим значением, используя сравнение Object.is. Если вы опустите этот аргумент, ваш Effect будет запускаться заново после каждого повторного рендеринга компонента.

Возврат¶

useEffect возвращает undefined.

Ограничения¶

  • useEffect — это хук, поэтому вы можете вызывать его только на верхнем уровне вашего компонента или ваших собственных хуков. Вы не можете вызывать его внутри циклов или условий. Если вам это нужно, создайте новый компонент и переместите состояние в него.

  • Если вы не пытаетесь синхронизироваться с какой-то внешней системой, вам, вероятно, не нужен Эффект.

  • Когда включен строгий режим, React будет проводить один дополнительный цикл настройки+очистки только для разработки перед первой реальной настройкой. Это стресс-тест, который гарантирует, что ваша логика очистки «отражает» вашу логику настройки и что она останавливает или отменяет все, что делает настройка. Если это вызывает проблему, реализуйте функцию очистки.

  • Если некоторые из ваших зависимостей являются объектами или функциями, определенными внутри компонента, есть риск, что они приведут к тому, что Эффект будет перезапускаться чаще, чем нужно. Чтобы исправить это, удалите ненужные зависимости object и function. Вы также можете извлекать обновления состояния и нереактивную логику вне вашего Эффекта.

  • Если ваш Эффект не был вызван взаимодействием (например, щелчком мыши), React позволит браузеру сначала нарисовать обновленный экран, прежде чем запустить ваш Эффект. Если ваш Эффект делает что-то визуальное (например, позиционирует всплывающую подсказку), и задержка заметна (например, она мерцает), замените useEffect на useLayoutEffect.

  • Даже если ваш Эффект был вызван взаимодействием (например, щелчком), браузер может перерисовать экран до обработки обновлений состояния внутри вашего Эффекта. Обычно это то, что вам нужно. Однако, если вы должны запретить браузеру перерисовывать экран, вам нужно заменить useEffect на useLayoutEffect.

  • Эффекты работают только на клиенте. Они не работают во время серверного рендеринга.

Использование¶

Подключение к внешней системе¶

Некоторые компоненты должны оставаться подключенными к сети, API браузера или сторонней библиотеке, пока они отображаются на странице. Эти системы не контролируются React, поэтому их называют внешними.

Чтобы подключить ваш компонент к какой-либо внешней системе, вызовите useEffect на верхнем уровне вашего компонента:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
    const [serverUrl, setServerUrl] = useState(
        'https://localhost:1234'
    );

    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        );
        connection.connect();
        return () => {
            connection.disconnect();
        };
    }, [serverUrl, roomId]);
    // ...
}

Вам необходимо передать два аргумента в useEffect:

  1. Функция setup с setup code, которая подключается к этой системе.
    • Она должна возвращать функцию очистки с кодом очистки, которая отсоединяется от этой системы.
  2. Список зависимостей, включающий каждое значение из вашего компонента, используемое внутри этих функций.

React вызывает ваши функции установки и очистки всякий раз, когда это необходимо, что может происходить несколько раз:.

  1. Ваш код установки выполняется, когда ваш компонент добавляется на страницу (mounts).
  2. После каждого повторного рендеринга вашего компонента, когда зависимости изменились:
    • Сначала ваш cleanup code запускается со старыми пропсами и состоянием.
    • Затем, ваш setup code запускается с новыми пропсами и состоянием.
  3. Ваш cleanup code запускается в последний раз после того, как ваш компонент удаляется со страницы (размонтируется).

Давайте проиллюстрируем эту последовательность на примере выше.

Когда вышеуказанный компонент ChatRoom добавляется на страницу, он подключается к чату с начальными serverUrl и roomId. Если serverUrl или roomId изменятся в результате повторного рендеринга (например, если пользователь выберет другую комнату в выпадающем списке), ваш Эффект отключится от предыдущей комнаты и подключится к следующей. Когда компонент ChatRoom будет удален со страницы, ваш Эффект отключится в последний раз.

Чтобы помочь вам найти ошибки, при разработке React запускает setup и cleanup один дополнительный раз перед setup. Это стресс-тест, который проверяет правильность реализации логики вашего Эффекта. Если это вызывает видимые проблемы, значит, вашей функции очистки не хватает логики. Функция очистки должна остановить или отменить все, что делала функция настройки. Эмпирическое правило гласит, что пользователь не должен различать между однократным вызовом функции setup (как в продакшене) и последовательностью setupcleanupsetup (как в разработке). См. общие решения.

Попробуйте писать каждый эффект как независимый процесс и думать об одном цикле установки/очистки за раз Не должно иметь значения, монтируется, обновляется или размонтируется ваш компонент. Когда ваша логика очистки правильно «зеркалит» логику установки, ваш Эффект устойчив к запуску установки и очистки так часто, как это необходимо.

Примеры подключения к внешней системе¶

1. Подключение к чат-серверу¶

В этом примере компонент ChatRoom использует Effect, чтобы оставаться подключенным к внешней системе, определенной в chat.js. Нажмите «Открыть чат», чтобы появился компонент ChatRoom. Эта песочница работает в режиме разработки, поэтому существует дополнительный цикл подключения и отключения, как объясняется здесь Попробуйте изменить roomId и erverUrl с помощью выпадающего списка и ввода, и посмотрите, как Эффект снова подключается к чату. Нажмите «Закрыть чат», чтобы увидеть, как Эффект отключится в последний раз.

App.jschat.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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} />}
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function createConnection(serverUrl, roomId) {
    // A real implementation would actually connect to the server
    return {
        connect() {
            console.log(
                '✅ Connecting to "' +
                    roomId +
                    '" room at ' +
                    serverUrl +
                    '...'
            );
        },
        disconnect() {
            console.log(
                '❌ Disconnected from "' +
                    roomId +
                    '" room at ' +
                    serverUrl
            );
        },
    };
}

2. Прослушивание глобального события браузера¶

В этом примере внешней системой является сам DOM браузера. Обычно вы указываете слушателей событий с помощью JSX, но вы не можете прослушивать глобальный объект window таким образом. Эффект позволяет вам подключиться к объекту window и прослушивать его события. Прослушивание события pointermove позволяет отслеживать положение курсора (или пальца) и обновлять красную точку, чтобы она двигалась вместе с ним.

App.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { useState, useEffect } from 'react';

export default function App() {
    const [position, setPosition] = useState({
        x: 0,
        y: 0,
    });

    useEffect(() => {
        function handleMove(e) {
            setPosition({ x: e.clientX, y: e.clientY });
        }
        window.addEventListener('pointermove', handleMove);
        return () => {
            window.removeEventListener(
                'pointermove',
                handleMove
            );
        };
    }, []);

    return (
        <div
            style={{
                position: 'absolute',
                backgroundColor: 'pink',
                borderRadius: '50%',
                opacity: 0.6,
                transform: `translate(${position.x}px, ${position.y}px)`,
                pointerEvents: 'none',
                left: -20,
                top: -20,
                width: 40,
                height: 40,
            }}
        />
    );
}

3. Запуск анимации¶

В этом примере внешней системой является библиотека анимации в animation.js. Она предоставляет класс JavaScript под названием FadeInAnimation, который принимает узел DOM в качестве аргумента и раскрывает методы start() и stop() для управления анимацией. Этот компонент использует ссылку для доступа к базовому узлу DOM. Эффект считывает узел DOM из ссылки и автоматически запускает анимацию для этого узла при появлении компонента.

App.jsanimation.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { useState, useEffect, useRef } from 'react';
import { FadeInAnimation } from './animation.js';

function Welcome() {
    const ref = useRef(null);

    useEffect(() => {
        const animation = new FadeInAnimation(ref.current);
        animation.start(1000);
        return () => {
            animation.stop();
        };
    }, []);

    return (
        <h1
            ref={ref}
            style={{
                opacity: 0,
                color: 'white',
                padding: 50,
                textAlign: 'center',
                fontSize: 50,
                backgroundImage:
                    'radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%)',
            }}
        >
            Welcome
        </h1>
    );
}

export default function App() {
    const [show, setShow] = useState(false);
    return (
        <>
            <button onClick={() => setShow(!show)}>
                {show ? 'Remove' : 'Show'}
            </button>
            <hr />
            {show && <Welcome />}
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
export class FadeInAnimation {
    constructor(node) {
        this.node = node;
    }
    start(duration) {
        this.duration = duration;
        if (this.duration === 0) {
            // Jump to end immediately
            this.onProgress(1);
        } else {
            this.onProgress(0);
            // Start animating
            this.startTime = performance.now();
            this.frameId = requestAnimationFrame(() =>
                this.onFrame()
            );
        }
    }
    onFrame() {
        const timePassed =
            performance.now() - this.startTime;
        const progress = Math.min(
            timePassed / this.duration,
            1
        );
        this.onProgress(progress);
        if (progress < 1) {
            // We still have more frames to paint
            this.frameId = requestAnimationFrame(() =>
                this.onFrame()
            );
        }
    }
    onProgress(progress) {
        this.node.style.opacity = progress;
    }
    stop() {
        cancelAnimationFrame(this.frameId);
        this.startTime = null;
        this.frameId = null;
        this.duration = 0;
    }
}

4. Управление модальным диалогом¶

В этом примере внешней системой является DOM браузера. Компонент ModalDialog отображает элемент <dialog>. Он использует Effect для синхронизации свойства isOpen с вызовами методов showModal() и close().

App.jsModalDialog.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { useState } from 'react';
import ModalDialog from './ModalDialog.js';

export default function App() {
    const [show, setShow] = useState(false);
    return (
        <>
            <button onClick={() => setShow(true)}>
                Open dialog
            </button>
            <ModalDialog isOpen={show}>
                Hello there!
                <br />
                <button
                    onClick={() => {
                        setShow(false);
                    }}
                >
                    Close
                </button>
            </ModalDialog>
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { useEffect, useRef } from 'react';

export default function ModalDialog({ isOpen, children }) {
    const ref = useRef();

    useEffect(() => {
        if (!isOpen) {
            return;
        }
        const dialog = ref.current;
        dialog.showModal();
        return () => {
            dialog.close();
        };
    }, [isOpen]);

    return <dialog ref={ref}>{children}</dialog>;
}

Отслеживание видимости элемента¶

В этом примере внешней системой снова является DOM браузера. Компонент App отображает длинный список, затем компонент Box, а затем еще один длинный список. Прокрутите список вниз. Обратите внимание, что когда компонент Box появляется в области просмотра, цвет фона меняется на черный. Чтобы реализовать это, компонент Box использует Effect для управления IntersectionObserver. Этот API браузера уведомляет вас, когда элемент DOM становится видимым в области просмотра.

App.jsBox.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Box from './Box.js';

export default function App() {
    return (
        <>
            <LongSection />
            <Box />
            <LongSection />
            <Box />
            <LongSection />
        </>
    );
}

function LongSection() {
    const items = [];
    for (let i = 0; i < 50; i++) {
        items.push(
            <li key={i}>Item #{i} (keep scrolling)</li>
        );
    }
    return <ul>{items}</ul>;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { useRef, useEffect } from 'react';

export default function Box() {
    const ref = useRef(null);

    useEffect(() => {
        const div = ref.current;
        const observer = new IntersectionObserver(
            (entries) => {
                const entry = entries[0];
                if (entry.isIntersecting) {
                    document.body.style.backgroundColor =
                        'black';
                    document.body.style.color = 'white';
                } else {
                    document.body.style.backgroundColor =
                        'white';
                    document.body.style.color = 'black';
                }
            }
        );
        observer.observe(div, {
            threshold: 1.0,
        });
        return () => {
            observer.disconnect();
        };
    }, []);

    return (
        <div
            ref={ref}
            style={{
                margin: 20,
                height: 100,
                width: 100,
                border: '2px solid black',
                backgroundColor: 'blue',
            }}
        />
    );
}

Обертывание эффектов в пользовательские хуки¶

Эффекты — это «аварийный люк»: Вы используете их, когда вам нужно «выйти за пределы React» и когда нет лучшего встроенного решения для вашего случая использования. Если вы часто сталкиваетесь с необходимостью вручную писать Effects, это обычно признак того, что вам нужно извлечь некоторые custom Hooks для общего поведения, на которое полагаются ваши компоненты.

Например, этот пользовательский хук useChatRoom «прячет» логику вашего Эффекта за более декларативным API:

function useChatRoom({ serverUrl, roomId }) {
    useEffect(() => {
        const options = {
            serverUrl: serverUrl,
            roomId: roomId,
        };
        const connection = createConnection(options);
        connection.connect();
        return () => connection.disconnect();
    }, [roomId, serverUrl]);
}

Затем вы можете использовать его из любого компонента, как это сделано здесь:

function ChatRoom({ roomId }) {
    const [serverUrl, setServerUrl] = useState(
        'https://localhost:1234'
    );

    useChatRoom({
        roomId: roomId,
        serverUrl: serverUrl,
    });
    // ...
}

В экосистеме React также существует множество отличных пользовательских хуков для любых целей.

Подробнее об обертывании эффектов в пользовательские хуки

Примеры обертывания эффектов в пользовательские хуки¶

1. Пользовательский хук useChatRoom

Этот пример идентичен одному из предыдущих примеров, но логика вынесена в пользовательский хук.

App.jsuseChatRoom.jschat.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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} />}
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { useEffect } from 'react';
import { createConnection } from './chat.js';

export function useChatRoom({ serverUrl, roomId }) {
    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        );
        connection.connect();
        return () => {
            connection.disconnect();
        };
    }, [roomId, serverUrl]);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function createConnection(serverUrl, roomId) {
    // A real implementation would actually connect to the server
    return {
        connect() {
            console.log(
                '✅ Connecting to "' +
                    roomId +
                    '" room at ' +
                    serverUrl +
                    '...'
            );
        },
        disconnect() {
            console.log(
                '❌ Disconnected from "' +
                    roomId +
                    '" room at ' +
                    serverUrl
            );
        },
    };
}

2. Пользовательский хук useWindowListener

Этот пример идентичен одному из предыдущих примеров, но логика вынесена в пользовательский хук.

App.jsuseWindowListener.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { useState } from 'react';
import { useWindowListener } from './useWindowListener.js';

export default function App() {
    const [position, setPosition] = useState({
        x: 0,
        y: 0,
    });

    useWindowListener('pointermove', (e) => {
        setPosition({ x: e.clientX, y: e.clientY });
    });

    return (
        <div
            style={{
                position: 'absolute',
                backgroundColor: 'pink',
                borderRadius: '50%',
                opacity: 0.6,
                transform: `translate(${position.x}px, ${position.y}px)`,
                pointerEvents: 'none',
                left: -20,
                top: -20,
                width: 40,
                height: 40,
            }}
        />
    );
}
import { useState, useEffect } from 'react';

export function useWindowListener(eventType, listener) {
    useEffect(() => {
        window.addEventListener(eventType, listener);
        return () => {
            window.removeEventListener(eventType, listener);
        };
    }, [eventType, listener]);
}

3. Пользовательский хук useIntersectionObserver

Этот пример идентичен одному из предыдущих примеров, но логика частично вынесена в пользовательский хук.

App.jsBox.jsuseIntersectionObserver.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Box from './Box.js';

export default function App() {
    return (
        <>
            <LongSection />
            <Box />
            <LongSection />
            <Box />
            <LongSection />
        </>
    );
}

function LongSection() {
    const items = [];
    for (let i = 0; i < 50; i++) {
        items.push(
            <li key={i}>Item #{i} (keep scrolling)</li>
        );
    }
    return <ul>{items}</ul>;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { useRef, useEffect } from 'react';
import { useIntersectionObserver } from './useIntersectionObserver.js';

export default function Box() {
    const ref = useRef(null);
    const isIntersecting = useIntersectionObserver(ref);

    useEffect(() => {
        if (isIntersecting) {
            document.body.style.backgroundColor = 'black';
            document.body.style.color = 'white';
        } else {
            document.body.style.backgroundColor = 'white';
            document.body.style.color = 'black';
        }
    }, [isIntersecting]);

    return (
        <div
            ref={ref}
            style={{
                margin: 20,
                height: 100,
                width: 100,
                border: '2px solid black',
                backgroundColor: 'blue',
            }}
        />
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useState, useEffect } from 'react';

export function useIntersectionObserver(ref) {
    const [isIntersecting, setIsIntersecting] = useState(
        false
    );

    useEffect(() => {
        const div = ref.current;
        const observer = new IntersectionObserver(
            (entries) => {
                const entry = entries[0];
                setIsIntersecting(entry.isIntersecting);
            }
        );
        observer.observe(div, {
            threshold: 1.0,
        });
        return () => {
            observer.disconnect();
        };
    }, [ref]);

    return isIntersecting;
}

Управление виджетом без React¶

Иногда вы хотите, чтобы внешняя система синхронизировалась с каким-либо параметром или состоянием вашего компонента.

Например, если у вас есть сторонний виджет карты или компонент видеоплеера, написанный без React, вы можете использовать Effect для вызова методов, которые заставят его состояние соответствовать текущему состоянию вашего компонента React. Этот Эффект создает экземпляр класса MapWidget, определенного в map-widget.js. Когда вы изменяете параметр zoomLevel компонента Map, Эффект вызывает setZoom() для экземпляра класса, чтобы сохранить его синхронизацию:

App.jsMap.jsmap-widget.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useState } from 'react';
import Map from './Map.js';

export default function App() {
    const [zoomLevel, setZoomLevel] = useState(0);
    return (
        <>
            Zoom level: {zoomLevel}x
            <button
                onClick={() => setZoomLevel(zoomLevel + 1)}
            >
                +
            </button>
            <button
                onClick={() => setZoomLevel(zoomLevel - 1)}
            >
                -
            </button>
            <hr />
            <Map zoomLevel={zoomLevel} />
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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}
        />
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import 'leaflet/dist/leaflet.css';
import * as L from 'leaflet';

export class MapWidget {
    constructor(domNode) {
        this.map = L.map(domNode, {
            zoomControl: false,
            doubleClickZoom: false,
            boxZoom: false,
            keyboard: false,
            scrollWheelZoom: false,
            zoomAnimation: false,
            touchZoom: false,
            zoomSnap: 0.1,
        });
        L.tileLayer(
            'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
            {
                maxZoom: 19,
                attribution: '© OpenStreetMap',
            }
        ).addTo(this.map);
        this.map.setView([0, 0], 0);
    }
    setZoom(level) {
        this.map.setZoom(level);
    }
}

В данном примере функция очистки не нужна, поскольку класс MapWidget управляет только узлом DOM, который был ему передан. После удаления React-компонента Map из дерева, и DOM-узел, и экземпляр класса MapWidget будут автоматически очищены от мусора JavaScript-движком браузера.

Получение данных с помощью эффектов¶

Вы можете использовать Эффект для получения данных для вашего компонента. Обратите внимание, что если вы используете фреймворк, использование механизма получения данных вашего фреймворка будет намного эффективнее, чем написание эффектов вручную.

Если вы хотите получить данные из Эффекта вручную, ваш код может выглядеть следующим образом:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
    const [person, setPerson] = useState('Alice');
    const [bio, setBio] = useState(null);

    useEffect(() => {
        let ignore = false;
        setBio(null);
        fetchBio(person).then((result) => {
            if (!ignore) {
                setBio(result);
            }
        });
        return () => {
            ignore = true;
        };
    }, [person]);
    // ...
}

Обратите внимание на переменную ignore, которая инициализируется в false и устанавливается в true во время очистки. Это гарантирует, что ваш код не пострадает от «условий гонки»: ответы сети могут приходить не в том порядке, в котором вы их отправили.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
    const [person, setPerson] = useState('Alice');
    const [bio, setBio] = useState(null);
    useEffect(() => {
        let ignore = false;
        setBio(null);
        fetchBio(person).then((result) => {
            if (!ignore) {
                setBio(result);
            }
        });
        return () => {
            ignore = true;
        };
    }, [person]);

    return (
        <>
            <select
                value={person}
                onChange={(e) => {
                    setPerson(e.target.value);
                }}
            >
                <option value="Alice">Alice</option>
                <option value="Bob">Bob</option>
                <option value="Taylor">Taylor</option>
            </select>
            <hr />
            <p>
                <i>{bio ?? 'Loading...'}</i>
            </p>
        </>
    );
}

Вы также можете переписать, используя синтаксис async / await, но вам все равно придется предоставить функцию очистки:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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>
        </>
    );
}

Запись получения данных непосредственно в Effects становится повторяющейся и затрудняет добавление таких оптимизаций, как кэширование и серверный рендеринг. Проще использовать пользовательский хук — либо свой собственный, либо поддерживаемый сообществом.

Какие есть хорошие альтернативы для получения данных в Effects?

Написание вызовов fetch внутри Effects является популярным способом получения данных, особенно в полностью клиентских приложениях. Однако это очень ручной подход, и у него есть существенные недостатки:

  • Эффекты не запускаются на сервере. Это означает, что первоначальный HTML, отрисованный на сервере, будет содержать только состояние загрузки без каких-либо данных. Клиентский компьютер должен будет загрузить весь JavaScript и отобразить ваше приложение только для того, чтобы обнаружить, что теперь ему нужно загрузить данные. Это не очень эффективно.
  • Получение данных непосредственно в Effects позволяет легко создавать «сетевые водопады». Вы рендерите родительский компонент, он получает некоторые данные, рендерит дочерние компоненты, а затем они начинают получать свои данные. Если сеть не очень быстрая, это значительно медленнее, чем параллельная выборка всех данных.
  • Например, если компонент размонтируется, а затем снова монтируется, ему придется снова получать данные.
  • Это не очень эргономично. Существует довольно много кода, связанного с написанием вызовов fetch таким образом, чтобы не страдать от ошибок типа race conditions.

Этот список недостатков не является специфическим для React. Он применим к выборке данных при подключении с помощью любой библиотеки. Как и в случае с маршрутизацией, выборка данных не является тривиальной задачей, поэтому мы рекомендуем следующие подходы:

  • Если вы используете фреймворк, используйте его встроенный механизм выборки данных. Современные фреймворки React имеют встроенные механизмы выборки данных, которые эффективны и не страдают от описанных выше подводных камней.
  • В противном случае, рассмотрите возможность использования или создания кэша на стороне клиента.** Популярные решения с открытым исходным кодом включают React Query, useSWR и React Router 6.4+. Вы также можете создать собственное решение, в этом случае вы будете использовать Effects под капотом, но также добавите логику для дедупликации запросов, кэширования ответов и избежания сетевых водопадов (путем предварительной загрузки данных или поднятия требований к данным в маршрутах).

Вы можете продолжать получать данные непосредственно в Effects, если ни один из этих подходов вам не подходит.

Указание реактивных зависимостей¶

Обратите внимание, что вы не можете «выбрать» зависимости вашего Эффекта. Каждый реактивное значение, используемое вашим Эффектом, может быть использован в качестве зависимого.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function ChatRoom({ roomId }) {
    // This is a reactive value
    const [serverUrl, setServerUrl] = useState(
        'https://localhost:1234'
    ); // This is a reactive value too

    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        ); // This Effect reads these reactive values
        connection.connect();
        return () => connection.disconnect();
    }, [serverUrl, roomId]);
    // ✅ So you must specify them as dependencies of your Effect
    // ...
}

Если serverUrl или roomId изменятся, ваш Эффект переподключится к чату, используя новые значения.

Реактивные значения включают пропсы и все переменные и функции, объявленные непосредственно внутри вашего компонента. Поскольку roomId и erverUrl являются реактивными значениями, вы не можете удалить их из зависимостей. Если вы попытаетесь опустить их и ваш линтер правильно настроен для React, линтер отметит это как ошибку, которую нужно исправить:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function ChatRoom({ roomId }) {
    const [serverUrl, setServerUrl] = useState(
        'https://localhost:1234'
    );

    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        );
        connection.connect();
        return () => connection.disconnect();
    }, []); // 🔴 React Hook useEffect has missing dependencies: 'roomId' and 'serverUrl'
    // ...
}

Чтобы удалить зависимость, вам нужно «доказать» линтеру, что она не должна быть зависимостью Например, вы можете убрать serverUrl из вашего компонента, чтобы доказать, что он не реактивный и не будет меняться при повторных рендерах:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore

function ChatRoom({ roomId }) {
    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        );
        connection.connect();
        return () => connection.disconnect();
    }, [roomId]); // ✅ All dependencies declared
    // ...
}

Теперь, когда serverUrl не является реактивным значением (и не может меняться при повторном рендере), ему не нужно быть зависимостью. Если код вашего Эффекта не использует никаких реактивных значений, его список зависимостей должен быть пустым ([]):.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
const roomId = 'music'; // Not a reactive value anymore

function ChatRoom() {
    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        );
        connection.connect();
        return () => connection.disconnect();
    }, []); // ✅ All dependencies declared
    // ...
}

Эффект с пустыми зависимостями не перезапускается при изменении пропсов или состояния вашего компонента.

Если у вас есть существующая кодовая база, у вас могут быть некоторые Эффекты, которые подавляют линтер подобным образом:

useEffect(() => {
    // ...
    // 🔴 Avoid suppressing the linter like this:
    // eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

Когда зависимости не соответствуют коду, существует высокий риск появления ошибок. Подавляя линтер, вы «лжете» React о значениях, от которых зависит ваш Эффект. Вместо этого докажите, что они не нужны.

Примеры передачи реактивных зависимостей¶

1. Передача массива зависимостей¶

Если вы укажете зависимости, ваш Эффект запустится после первоначального рендера и после повторных рендеров с измененными зависимостями.

useEffect(() => {
    // ...
}, [a, b]); // Runs again if a or b are different

В приведенном ниже примере serverUrl и roomId являются реактивными значениями, поэтому они оба должны быть указаны как зависимости. В результате выбор другой комнаты в выпадающем списке или редактирование URL сервера приводит к повторному подключению чата. Однако, поскольку message не используется в эффекте (и поэтому не является зависимостью), редактирование сообщения не приводит к повторному подключению к чату.

App.jschat.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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} />}
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function createConnection(serverUrl, roomId) {
    // A real implementation would actually connect to the server
    return {
        connect() {
            console.log(
                '✅ Connecting to "' +
                    roomId +
                    '" room at ' +
                    serverUrl +
                    '...'
            );
        },
        disconnect() {
            console.log(
                '❌ Disconnected from "' +
                    roomId +
                    '" room at ' +
                    serverUrl
            );
        },
    };
}

2. Передача пустого массива зависимостей¶

Если ваш Эффект действительно не использует никаких реактивных значений, он будет запущен только после начального рендеринга.

useEffect(() => {
    // ...
}, []); // Does not run again (except once in development)

Даже при пустых зависимостях, setup и cleanup будут выполняться один дополнительный раз в разработке, чтобы помочь вам найти ошибки..

В этом примере и serverUrl и roomId жестко закодированы. Поскольку они объявлены вне компонента, они не являются реактивными значениями, а значит, не являются зависимостями. Список зависимостей пуст, поэтому Effect не запускается при повторном рендере.

App.jschat.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';
const roomId = 'music';

function ChatRoom() {
    const [message, setMessage] = useState('');

    useEffect(() => {
        const connection = createConnection(
            serverUrl,
            roomId
        );
        connection.connect();
        return () => connection.disconnect();
    }, []);

    return (
        <>
            <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);
    return (
        <>
            <button onClick={() => setShow(!show)}>
                {show ? 'Close chat' : 'Open chat'}
            </button>
            {show && <hr />}
            {show && <ChatRoom />}
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function createConnection(serverUrl, roomId) {
    // A real implementation would actually connect to the server
    return {
        connect() {
            console.log(
                '✅ Connecting to "' +
                    roomId +
                    '" room at ' +
                    serverUrl +
                    '...'
            );
        },
        disconnect() {
            console.log(
                '❌ Disconnected from "' +
                    roomId +
                    '" room at ' +
                    serverUrl
            );
        },
    };
}

Непередача массива зависимостей вообще¶

Если вы не передаете массив зависимостей вообще, ваш Effect запускается после каждого рендера (и повторного рендера) вашего компонента.

useEffect(() => {
    // ...
}); // Always runs again

В этом примере Effect повторно запускается при изменении serverUrl и roomId, что вполне разумно. Однако он также запускается повторно, когда вы изменяете message, что, вероятно, нежелательно. Вот почему обычно указывается массив зависимостей.

App.jschat.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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();
        };
    }); // No dependency array at all

    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} />}
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function createConnection(serverUrl, roomId) {
    // A real implementation would actually connect to the server
    return {
        connect() {
            console.log(
                '✅ Connecting to "' +
                    roomId +
                    '" room at ' +
                    serverUrl +
                    '...'
            );
        },
        disconnect() {
            console.log(
                '❌ Disconnected from "' +
                    roomId +
                    '" room at ' +
                    serverUrl
            );
        },
    };
}

Обновление состояния на основе предыдущего состояния от Эффекта¶

Когда вы хотите обновить состояние на основе предыдущего состояния Эффекта, вы можете столкнуться с проблемой:

function Counter() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        const intervalId = setInterval(() => {
            setCount(count + 1); // You want to increment the counter every second...
        }, 1000);
        return () => clearInterval(intervalId);
    }, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval.
    // ...
}

Поскольку count является реактивным значением, оно должно быть указано в списке зависимостей. Однако это заставляет Effect очищать и устанавливать заново каждый раз, когда изменяется count. Это не идеально.

Чтобы исправить это, передайте функцию обновления состояния c => c + 1 в setCount:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { useState, useEffect } from 'react';

export default function Counter() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        const intervalId = setInterval(() => {
            setCount((c) => c + 1); // ✅ Pass a state updater
        }, 1000);
        return () => clearInterval(intervalId);
    }, []); // ✅ Now count is not a dependency

    return <h1>{count}</h1>;
}

Теперь, когда вы передаете c => c + 1 вместо count + 1, вашему Эффекту больше не нужно зависеть от count. В результате этого исправления, ему не нужно будет очищать и устанавливать интервал заново каждый раз, когда count меняется.

Удаление ненужных объектных зависимостей¶

Если ваш Эффект зависит от объекта или функции, созданной во время рендеринга, он может запускаться слишком часто. Например, этот Эффект переподключается после каждого рендеринга, потому что объект options разный для каждого рендеринга:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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
    // ...
}

Избегайте использования объекта, созданного во время рендеринга, в качестве зависимого. Вместо этого создайте объект внутри Effect:

App.jschat.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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} />
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function createConnection({ serverUrl, roomId }) {
    // A real implementation would actually connect to the server
    return {
        connect() {
            console.log(
                '✅ Connecting to "' +
                    roomId +
                    '" room at ' +
                    serverUrl +
                    '...'
            );
        },
        disconnect() {
            console.log(
                '❌ Disconnected from "' +
                    roomId +
                    '" room at ' +
                    serverUrl
            );
        },
    };
}

Теперь, когда вы создаете объект options внутри эффекта, сам эффект зависит только от строки roomId.

С этим исправлением ввод ввода не переподключает чат. В отличие от объекта, который создается заново, строка типа roomId не меняется, пока вы не зададите ей другое значение. Подробнее об удалении зависимостей.

Удаление ненужных зависимостей от функций¶

Если ваш Эффект зависит от объекта или функции, созданной во время рендеринга, он может запускаться слишком часто. Например, этот Эффект переподключается после каждого рендера, потому что функция createOptions разная для каждого рендера:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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
    // ...
}

Само по себе создание функции с нуля при каждом повторном рендере не является проблемой. Вам не нужно ее оптимизировать. Однако если вы используете ее в качестве зависимости от вашего Эффекта, это приведет к тому, что ваш Эффект будет запускаться заново после каждого повторного рендеринга.

Избегайте использования функции, созданной во время рендеринга, в качестве зависимости. Вместо этого объявите ее внутри Эффекта:

App.jschat.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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} />
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function createConnection({ serverUrl, roomId }) {
    // A real implementation would actually connect to the server
    return {
        connect() {
            console.log(
                '✅ Connecting to "' +
                    roomId +
                    '" room at ' +
                    serverUrl +
                    '...'
            );
        },
        disconnect() {
            console.log(
                '❌ Disconnected from "' +
                    roomId +
                    '" room at ' +
                    serverUrl
            );
        },
    };
}

Теперь, когда вы определяете функцию createOptions внутри эффекта, сам эффект зависит только от строки roomId. С этим исправлением ввод в input не переподключает чат. В отличие от функции, которая создается заново, такая строка, как roomId, не меняется, пока вы не установите для нее другое значение. Подробнее об удалении зависимостей.

Чтение последних пропсов и состояния из эффекта¶

Этот раздел описывает экспериментальный API, который еще не был выпущен в стабильной версии React.

По умолчанию, когда вы читаете реактивное значение из Эффекта, вы должны добавить его как зависимость. Это гарантирует, что ваш Эффект «реагирует» на каждое изменение этого значения. Для большинства зависимостей это именно то поведение, которое вам нужно.

Однако иногда вы хотите читать последние пропсы и состояния из Эффекта без «реакции» на них. Например, представьте, что вы хотите регистрировать количество товаров в корзине при каждом посещении страницы:

function Page({ url, shoppingCart }) {
    useEffect(() => {
        logVisit(url, shoppingCart.length);
    }, [url, shoppingCart]); // ✅ All dependencies declared
    // ...
}

Что если вы хотите регистрировать посещение новой страницы после каждого изменения url, но не, если меняется только shoppingCart? Вы не можете исключить shoppingCart из зависимостей, не нарушая правила реактивности. Однако вы можете выразить, что вы не хотите, чтобы часть кода «реагировала» на изменения, даже если она вызывается из Эффекта. Объявите событие Эффекта с помощью хука useEffectEvent и переместите код, читающий shoppingCart, внутрь него:

function Page({ url, shoppingCart }) {
    const onVisit = useEffectEvent((visitedUrl) => {
        logVisit(visitedUrl, shoppingCart.length);
    });

    useEffect(() => {
        onVisit(url);
    }, [url]); // ✅ All dependencies declared
    // ...
}

События Эффекта не являются реактивными и всегда должны быть исключены из зависимостей вашего Эффекта. Именно это позволяет вам поместить нереактивный код (где вы можете прочитать последнее значение некоторых пропсов и состояния) внутрь них. Читая shoppingCart внутри onVisit, вы гарантируете, что shoppingCart не будет повторно запускать ваш Эффект.

Подробнее о том, как события Effect Events позволяют разделить реактивный и нереактивный код

Отображение разного содержимого на сервере и клиенте¶

Если ваше приложение использует серверный рендеринг (либо напрямую, либо через фреймворк), ваш компонент будет отображаться в двух разных средах. На сервере он будет рендериться для создания исходного HTML. На клиенте React снова запустит код рендеринга, чтобы прикрепить обработчики событий к этому HTML. Вот почему, чтобы hydration работал, ваш первоначальный вывод рендеринга должен быть идентичным на клиенте и на сервере.

В редких случаях вам может понадобиться отобразить разное содержимое на клиенте. Например, если ваше приложение считывает некоторые данные из localStorage, оно не может сделать это на сервере. Вот как это можно реализовать:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function MyComponent() {
    const [didMount, setDidMount] = useState(false);

    useEffect(() => {
        setDidMount(true);
    }, []);

    if (didMount) {
        // ... return client-only JSX ...
    } else {
        // ... return initial JSX ...
    }
}

Пока приложение загружается, пользователь будет видеть начальный вывод рендера. Затем, после загрузки и гидратации, ваш Effect запустится и установит didMount в true, вызывая повторный рендеринг. При этом произойдет переключение на вывод рендера только для клиента. Эффекты не запускаются на сервере, поэтому didMount было false во время первоначального серверного рендеринга.

Используйте этот паттерн экономно. Помните, что пользователи с медленным соединением будут видеть начальное содержимое довольно долго — потенциально много секунд — поэтому вы не захотите вносить резкие изменения во внешний вид вашего компонента. Во многих случаях вы можете избежать этого, условно показывая различные вещи с помощью CSS.

Устранение неполадок¶

Мой эффект запускается дважды, когда компонент монтируется¶

Когда включен строгий режим, в процессе разработки React запускает установку и очистку один дополнительный раз перед фактической установкой.

Это стресс-тест, который проверяет правильность реализации логики вашего Эффекта. Если это вызывает видимые проблемы, значит, вашей функции очистки не хватает логики. Функция очистки должна остановить или отменить все, что делала функция настройки. Эмпирическое правило гласит, что пользователь не должен различать между однократным вызовом функции setup (как в производстве) и последовательностью setup → cleanup → setup (как в разработке).

Подробнее о как это помогает найти ошибки и как исправить логику

Мой эффект запускается после каждого рендера¶

Во-первых, проверьте, не забыли ли вы указать массив зависимостей:

useEffect(() => {
    // ...
}); // 🚩 No dependency array: re-runs after every render!

Если вы указали массив зависимостей, но ваш Effect все равно перезапускается в цикле, это связано с тем, что одна из ваших зависимостей отличается при каждом повторном рендере.

Вы можете отладить эту проблему, вручную записав журнал зависимостей в консоль:

useEffect(() => {
    // ..
}, [serverUrl, roomId]);

console.log([serverUrl, roomId]);

Затем вы можете щелкнуть правой кнопкой мыши на массивах из разных рендеров в консоли и выбрать «Store as a global variable» для обоих. Предположив, что первый массив был сохранен как temp1, а второй — как temp2, вы можете использовать консоль браузера, чтобы проверить, является ли каждая зависимость в обоих массивах одинаковой:

Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...

Когда вы обнаружите зависимость, которая отличается при каждом повторном рендере, вы обычно можете исправить ее одним из этих способов:

  • Обновление состояния на основе предыдущего состояния эффекта
  • Удаление ненужных объектных зависимостей
  • Удаление ненужных зависимостей функций
  • Чтение последних пропсов и состояния из эффекта

В крайнем случае (если эти методы не помогли), оберните его создание с помощью useMemo или useCallback (для функций).

Мой Эффект повторяется в бесконечном цикле¶

Если ваш Эффект запускается в бесконечном цикле, эти две вещи должны быть верны:

  • Ваш Эффект обновляет некоторое состояние.
  • Это состояние приводит к повторному рендерингу, что вызывает изменение зависимостей Эффекта.

Прежде чем приступить к устранению проблемы, спросите себя, подключается ли ваш Эффект к какой-либо внешней системе (например, DOM, сеть, сторонний виджет и так далее). Зачем вашему Эффекту нужно устанавливать состояние? Синхронизируется ли он с этой внешней системой? Или вы пытаетесь управлять потоком данных вашего приложения вместе с ней?

Если внешней системы нет, подумайте, не упростит ли вашу логику полное удаление эффекта.

Если вы действительно синхронизируетесь с какой-то внешней системой, подумайте, почему и при каких условиях ваш Эффект должен обновлять состояние. Изменилось ли что-то, что влияет на визуальный вывод вашего компонента? Если вам нужно отслеживать какие-то данные, которые не используются при визуализации, то более подходящим вариантом может быть ref (который не вызывает повторного рендеринга). Убедитесь, что ваш эффект не обновляет состояние (и не запускает повторные рендеринги) чаще, чем это необходимо.

Наконец, если ваш Эффект обновляет состояние в нужное время, но цикл все еще существует, это связано с тем, что обновление состояния приводит к изменению одной из зависимостей Эффекта.

Моя логика очистки выполняется, даже если компонент не был размонтирован¶

Функция очистки запускается не только во время размонтирования, но и перед каждым повторным рендерингом с измененными зависимостями. Кроме того, в процессе разработки React запускает setup+cleanup один дополнительный раз сразу после монтирования компонента.

Если у вас есть код очистки без соответствующего кода установки, это обычно запах кода:

useEffect(() => {
    // 🔴 Avoid: Cleanup logic without corresponding setup logic
    return () => {
        doSomething();
    };
}, []);

Ваша логика очистки должна быть «симметрична» логике настройки и должна останавливать или отменять все, что сделала настройка:

useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
        connection.disconnect();
    };
}, [serverUrl, roomId]);

Узнайте, чем жизненный цикл эффекта отличается от жизненного цикла компонента

Мой эффект делает что-то визуальное, и я вижу мерцание перед его запуском¶

Если ваш Эффект должен блокировать браузер от рисования экрана, замените useEffect на useLayoutEffect. Обратите внимание, что это не нужно для подавляющего большинства Эффектов. Это понадобится только в том случае, если очень важно запустить ваш Эффект до рисования браузера: например, для измерения и позиционирования всплывающей подсказки до того, как пользователь ее увидит.

Ссылки¶

  • https://react.dev/reference/react/useEffect

Вы написали несколько компонентов с использованием хуков. Возможно — даже создали небольшое приложение. В целом результат вас вполне устраивает. Вы привыкли к 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 в React

14 февраля 2023 г.

Хук React useEffect — один из самых сложных и мощных хуков в функциональных компонентах, поскольку он позволяет вам выполнять побочные эффекты в реакции. Но что такое побочный эффект? В React «побочный эффект» — это любая операция, которая влияет на что-то за пределами компонента, например вызов REST API, обновление DOM и т. д. Она будет выполняться после рендеринга компонента. Если вы знакомы с компонентами на основе классов, то ловушку useEffect можно легко понять как комбинацию методов жизненного цикла componentDidMount, componentDidUpdate и componentWillUnmount.

В этой статье мы подробно рассмотрим использование хука useEffect с примерами.

Когда использовать хук эффекта

Хук useEffect следует использовать каждый раз, когда вам нужен функциональный компонент для выполнения побочного эффекта. Это может включать такие вещи, как извлечение данных, настройка подписок и обновление ДОМ. Важно отметить, что хук useEffect не следует использовать для целей рендеринга, поскольку он не предназначен для замены механизма рендеринга React.

Некоторые сценарии, в которых может пригодиться хук useEffect, включают:

* Извлечение данных из API и обновление состояния компонента на основе ответа API.
* Настройка подписки на источник данных и обновление состояния компонента при получении новых данных.
* Извлечение/сохранение данных из localStorage
* Добавление и удаление прослушивателей событий.

синтаксис использования

Синтаксис хука useEffect следующий:

useEffect(() => {
  // function body
}, [dependencies]);

Хук useEffect вызывается внутри функционального компонента и принимает два аргумента: функцию, представляющую тело эффекта, и необязательный массив зависимостей. Функция эффекта выполняется после рендеринга компонента. Когда указан массив зависимостей и изменяются значения аргументов в массиве зависимостей, будет запущен повторный запуск эффекта.

Ниже приведен синтаксис хука useEffect с функцией очистки.

useEffect(() => {
  // effect function
  return () => {
    // cleanup function
  };
}, [dependencies]);

Функция эффекта может возвращать функцию cleanup, которая будет запущена перед повторным запуском эффекта или перед размонтированием компонента. Эту функцию очистки можно использовать для выполнения любых необходимых операций очистки, таких как отмена сетевых запросов, удаление прослушивателей событий, отказ от подписки на источники данных и т. д.

<цитата>

В одном и том же функциональном компоненте может быть несколько useEffect.

Как использовать хук эффекта

Чтобы использовать хук useEffect, сначала необходимо импортировать его из библиотеки react. Затем вы можете вызвать функцию useEffect в своем компоненте и передать функцию, которая представляет эффект, который вы хотите выполнить.

import { useEffect } from "react";

function MyComponent() {
  useEffect(() => {
    // Your effect function here
  }, []);

  return <div>Hello World</div>;
}

Давайте рассмотрим подробное использование useEffect с примерами,

Пример 1: без передачи массива зависимостей

Если массив зависимостей вообще не указан, то useEffect будет выполняться каждый раз при рендеринге компонента.

import { useEffect } from "react";

function MyComponent() {
  useEffect(() => {
    console.log("This will be run every time the component renders");
  });

  return <div>Hello World</div>;
}

Это нетипичный случай, и обычно мы не используем этот сценарий в приложениях реального времени.

Пример 2. Передача пустого массива зависимостей

При передаче пустого массива зависимостей хук useEffect будет выполняться только один раз, когда компонент монтируется в DOM. Допустим, нам нужно получить сообщения в блоге автора после того, как он войдет в систему. В этом сценарии достаточно получить сообщения в блоге только один раз, а не каждый раз при повторном отображении компонента.

import { useEffect, useState } from "react";

function Posts() {
  const [posts, setposts] = useState([]);
  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users/1/posts")
      .then((resp) => resp.json())
      .then((blogPosts) => setposts(blogPosts));
  }, []);

  return (
    <div className="App">
      {posts && posts.map((post) => <li>{post.title}</li>)}
    </div>
  );
}

export default Posts;

В приведенном выше примере мы извлекаем сообщения пользователя только один раз и отображаем их в DOM только один раз.

Некоторые другие сценарии, в которых вы будете передавать пустой массив зависимостей.

* Если вы хотите обновить заголовок страницы при посещении определенной страницы.
* Когда вы хотите отправлять аналитические данные на ваш сервер, когда пользователь посещает определенную страницу. (Например, счетчик посещений страницы)

Пример 3. Передача аргументов в массив зависимостей

Когда аргумент передается в массив зависимостей, это гарантирует, что эффект будет повторно запускаться при каждом изменении его значения.

Допустим, нам нужно реализовать функцию поиска, которая фильтрует статьи/сообщения в блогах на основе ключевого слова, введенного пользователем. В этом случае мы можем передать ключевое слово поиска в качестве аргумента и реализовать логику фильтра в теле эффекта.

import { useEffect, useState } from "react";

function Search() {
  const [posts, setposts] = useState([]);
  const [search, setsearch] = useState("");

  useEffect(() => {
    const filteredPosts = posts.filter((p) => p.title.includes(search));
    setposts(filteredPosts);
  }, [search]);

  return (
    <div className="App">
      {posts && (
        <input
          type="text"
          value={search}
          onChange={(e) => setsearch(e.target.value)}
        />
      )}
      {posts && posts.map((post) => <li>{post.title}</li>)}
    </div>
  );
}

export default Search;

Таким образом, всякий раз, когда пользователь вводит поисковый запрос, состояние search изменяется и приводит к повторному запуску эффекта.

Пример 4. С функцией очистки

Во всех приведенных выше примерах мы не использовали дополнительную функцию очистки. Но будет несколько случаев, когда нам может понадобиться использовать функцию очистки.

Допустим, нам нужно реализовать сценарий, в котором, когда пользователь нажимает кнопку, отображается раскрывающийся список. И когда пользователь щелкает в любом месте за пределами раскрывающегося списка, он должен автоматически закрыть раскрывающийся список. Для этого мы можем использовать прослушиватели событий.

import { useEffect, useRef, useState } from "react";

function Dropdown() {
  const ref = useRef(null);
  const [open, setOpen] = useState(false);
  const [options, setoptions] = useState([
    { key: 1, value: "Audi" },
    { key: 2, value: "BMW" },
    { key: 3, value: "Jaguar" },
    { key: 4, value: "Ferrari" }
  ]);
  const [option, setOption] = useState("");

  useEffect(() => {
    const handleClickOutside = (event) => {
      if (ref.current && !ref.current.contains(event.target)) {
        setOpen(false);
      }
    };
    document.addEventListener("click", handleClickOutside);
    return () => document.removeEventListener("click", handleClickOutside);
  }, []);

  return (
    <div ref={ref}>
      <button onClick={() => setOpen(!open)}>Toggle Dropdown</button>
      {open && (
        <ul>
          {options.map((option) => (
            <li key={option.key} onClick={() => setOption(option.value)}>
              {option.value}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default Dropdown;

В этом примере мы настроили прослушиватель событий DOM, который закрывает раскрывающийся список, когда пользователь щелкает за пределами элемента раскрывающегося списка. Пустой массив зависимостей гарантирует, что эффект запускается только один раз, при монтировании, а функция очистки используется для удаления прослушивателя событий при размонтировании компонента.

Некоторые другие сценарии, когда вы хотите реализовать функцию очистки.

* В приложении чата на основе сокета, когда пользователь покидает комнату чата, нам нужно реализовать функцию очистки, чтобы отключиться от сокета.
* Если вы используете хук useEffect для настройки подписки на события или данные, вы должны включить функцию очистки, которая отменяет подписку на эти события или данные, когда компонент отключается или эффект перезапускается.

Как не стоит использовать эффектный хук (с примерами)

В предыдущем разделе мы видели различные примеры использования хука useEffect. В этом разделе мы увидим «Как не надо», т.е. распространенные ошибки, которые допускают разработчики при использовании хука useEffect.

Пример 1:

import { useEffect, useState } from "react";

function MyComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(count + 1);
  });

  return <div>{count}</div>;
}

В этом примере хук useEffect вызывается без массива зависимостей, в результате чего функция эффекта выполняется при каждом рендеринге. Это приводит к бесконечному циклу, поскольку функция эффекта обновляет состояние счетчика, вызывая повторную визуализацию компонента и повторный запуск эффекта.

Пример 2. Пустой массив зависимостей не передается

Если вы не включите пустой массив зависимостей, когда это необходимо, useEffect будет повторно запускаться при каждом рендеринге, что может привести к проблемам с производительностью вашего приложения.

Например, рассмотрим тот же пример, который мы использовали в примере 2 предыдущего раздела, но без передачи массива зависимостей

import { useEffect, useState } from "react";

function Posts() {
  const [posts, setposts] = useState([]);
  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users/1/posts")
      .then((resp) => resp.json())
      .then((blogPosts) => setposts(blogPosts));
  });

  return (
    <div className="App">
      {posts && posts.map((post) => <li>{post.title}</li>)}
    </div>
  );
}

export default Posts;

Таким образом, в этом случае при каждом рендеринге компонента будет выполняться вызов API для извлечения данных из внутреннего API, который не нужен и потребляет дополнительный сетевой трафик, тем самым влияя на производительность приложения.

Пример 3. Добавление ненужных зависимостей

Если вы включите ненужные зависимости в массив зависимостей хука useEffect, эффект будет запущен повторно без необходимости и потенциально может вызвать проблемы с производительностью в вашем приложении.

import { useEffect } from "react";

function TodoList({ todos, filter }) {
  useEffect(() => {
    console.log("filtering todos");
    // filter todos
  }, [todos, filter]);

  return <div>{/* todo list JSX */}</div>;
}

В приведенном выше примере хук useEffect настроен на фильтрацию массива задач при изменении реквизитов задач или фильтра. Однако реквизит фильтра не используется в эффекте и поэтому не должен включаться в массив зависимостей. Это может привести к ненужному повторному запуску эффекта при изменении реквизита фильтра.

Пример 4. Без учета функций очистки

Если вы не включите функцию очистки в обработчик useEffect, но настроите любые ресурсы, которые необходимо очистить (например, прослушиватели событий DOM, интервалы, соединения сокетов и т. д.), это приведет к утечке памяти. и проблемы с производительностью.

Например, рассмотрим тот же сценарий, который мы использовали в примере 4 предыдущего раздела, но без использования функции очистки.

import { useEffect, useRef, useState } from "react";

function Dropdown() {
  const ref = useRef(null);
  const [open, setOpen] = useState(false);
  const [options, setoptions] = useState([
    { key: 1, value: "Audi" },
    { key: 2, value: "BMW" },
    { key: 3, value: "Jaguar" },
    { key: 4, value: "Ferrari" }
  ]);
  const [option, setOption] = useState("");

  useEffect(() => {
    const handleClickOutside = (event) => {
      if (ref.current && !ref.current.contains(event.target)) {
        setOpen(false);
      }
    };
    document.addEventListener("click", handleClickOutside);
    // No Cleanup function
  }, []);

  return (
    <div ref={ref}>
      <button onClick={() => setOpen(!open)}>Toggle Dropdown</button>
      {open && (
        <ul>
          {options.map((option) => (
            <li key={option.key} onClick={() => setOption(option.value)}>
              {option.value}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default Dropdown;

Если мы не включим функцию очистки, то прослушиватель событий DOM, который мы создали в теле эффекта, не будет удален при размонтировании компонента.

Если прослушиватель событий не удаляется при размонтировании компонента, он будет продолжать прослушивать щелчки по документу, даже если компонент больше не отображается. Это может привести к утечке памяти, поскольку прослушиватель событий будет продолжать потреблять ресурсы, даже если он больше не нужен. Таким образом, всегда необходимо включать функцию очистки в useEffect, которая удаляет любые прослушиватели событий DOM при размонтировании компонента. Это обеспечит правильную очистку прослушивателей событий и освобождение ресурсов, когда они больше не нужны.

Заключение

В этой статье мы рассмотрели использование useEffect и рассмотрели примеры того, как и как не использовать хук Effect. В заключение отметим, что хук useEffect — это мощный инструмент в React, позволяющий выполнять побочные эффекты в функциональных компонентах. Важно правильно использовать хук useEffect, чтобы избежать проблем с производительностью. Следуя рекомендациям и избегая распространенных ошибок, как описано в статье, вы сможете эффективно управлять побочными эффектами в своих проектах React.


Также опубликовано здесь


Оригинал

Понравилась статья? Поделить с друзьями:
  • Перемена юр адреса ооо пошаговая инструкция
  • Робот пылесос xiaomi mijia руководство
  • Инструкция по работе со смартфоном для чайников
  • Бесплатное руководство по ремонту автомобиля mazda
  • Ангиотрофин инструкция по применению цена отзывы аналоги