[Перевод] Полное руководство по useEffect

Вы написали несколько компонентов с использованием хуков. Возможно — даже создали небольшое приложение. В целом результат вас вполне устраивает. Вы привыкли к API и в процессе работы обнаружили несколько неочевидных полезных приёмов. Вы даже создали несколько собственных хуков и сократили свой код на 300 строк, поместив в них то, что раньше было представлено повторяющимися фрагментами программы. То, что вы сделали, вы показали коллегам. «Отлично получилось», — сказали они о вашем проекте.

bdvje1n-vjzezd47wnphch1wq8a.png
Но иногда, когда вы используете useEffect, составные части программных механизмов не особенно хорошо стыкуются друг с другом. Вам кажется, что вы что-то упускаете. Всё это похоже на работу с событиями жизненного цикла компонентов, основанных на классах…, но так ли это на самом деле?
Пытаясь понять — что именно вас не устраивает, вы замечаете, что задаётесь следующими вопросами:

  • Как воспроизвести componentDidMount с помощью useEffect?
  • Как правильно загружать данные внутри useEffect? Что такое []?
  • Нужно ли указывать функции в виде зависимостей эффектов?
  • Почему иногда программа попадает в бесконечный цикл повторной загрузки данных?
  • Почему иногда внутри эффектов видно старое состояние или встречаются старые свойства?


Когда я только начал использовать хуки, меня тоже мучили эти вопросы. Даже когда я готовил документацию, я не мог бы сказать, что в совершенстве владею некоторыми тонкостями. С тех пор у меня было несколько моментов, когда я, вдруг поняв что-то важное, прямо-таки хотел воскликнуть: «Эврика!». О том, что я в эти моменты осознал, я и хочу вам рассказать. То, что вы узнаете сейчас о useEffect, позволит вам совершенно чётко разглядеть очевидные ответы на вышеприведённые вопросы.

Но для того чтобы увидеть ответы на эти вопросы, нам сначала надо сделать шаг назад. Цель этой статьи не в том, чтобы дать её читателям некую пошаговую инструкцию по работе с useEffect. Она нацелена на то, чтобы помочь вам, что называется, «грокнуть» useEffect. И, честно говоря, тут не так много всего нужно изучить. На самом деле, большую часть времени мы потратим на забывание того, что знали раньше.

У меня в голове всё сошлось только после того, как я перестал смотреть на хук useEffect через призму знакомых мне методов жизненного цикла компонентов, основанных на классах.

«Ты должен забыть то, чему тебя учили»


habr.com/ru/company/ruvds/blog/445276/Йода

8c39befba869c3bae1a6b87c2d2589d1.jpg


Предполагается, что читатель этого материала в определённой степени знаком с 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 (
    

You clicked {count} times

); }


Внимательно присмотритесь к строке

You clicked {count} times

. Что она означает? «Наблюдает» ли каким-то образом константа count за изменениями в состоянии и обновляется ли она автоматически? Такое заключение можно считать чем-то вроде ценной первой идеи того, кто изучает React, но оно не является точной ментальной моделью происходящего.

В нашем примере count — это просто число. Это не некая магическая «привязка данных», не некий «объект-наблюдатель» или «прокси», или что угодно другое. Перед нами — старое доброе число, вроде этого:

const count = 42;
// ...

You clicked {count} times

// ...


Во время первого вывода компонента значение count, получаемое из useState(), равняется 0. Когда мы вызываем setCount(1), React снова вызывает компонент. В этот раз count будет равно 1. И так далее:

// Во время первого рендеринга
function Counter() {
  const count = 0; // Возвращено useState()
  // ...

  

You clicked {count} times

// ... } // После щелчка наша функция вызывается снова function Counter() { const count = 1; // Возвращено useState() // ...

You clicked {count} times

// ... } // После ещё одного щелчка функция вызывается снова function Counter() { const count = 2; // Возвращено useState() // ...

You clicked {count} times

// ... }


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

В результате эта строка не выполняет какую-то особую операцию привязки данных:

You clicked {count} times


Она лишь встраивает числовое значение в код, формируемый при рендеринге. Это число предоставляется средствами 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 (
    

You clicked {count} times

); }


Предположим, я выполню следующую последовательность действий:

  • Доведу значение count до 3, щёлкая по кнопке Click me.
  • Щёлкну по кнопке Show alert.
  • Увеличу значение до 5 до истечения таймаута.


46c55d5f1f749462b7a173f1e748e41e.gif


Увеличение значения 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);
  }
  // ...

  


Именно поэтому в этом примере обработчики событий «принадлежат» конкретным рендерам, а когда вы щёлкаете по кнопке, компонент использует состояние count из этих рендеров.

Внутри каждого конкретного рендера свойства и состояние всегда остаются одними и теми же. Но если в разных операциях рендеринга используются собственные свойства и состояние, то же самое происходит и с любыми механизмами, использующими их (включая обработчики событий). Они тоже «принадлежат» конкретным рендерам. Поэтому даже асинхронные функции внутри обработчиков событий будут «видеть» те же самые значения count.

Надо отметить, что в вышеприведённом примере я встроил конкретные значения count прямо в функции handleAlertClick. Эта «мысленная» замена нам не повредит, так как константа count не может изменяться в пределах конкретного рендера. Во-первых, это константа, во вторых — это число. Можно с уверенностью говорить о том, что так же можно размышлять и о других значениях, вроде объектов, но только в том случае, если мы примем за правило не выполнять изменения (мутации) состояния. При этом нас устраивает вызов setSomething(newObj) с новым объектом вместо изменения существующего, так как при таком подходе состояние, принадлежащее предыдущему рендеру, оказывается нетронутым.

У каждого рендера есть собственные эффекты


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

Рассмотрим пример из документации, который очень похож на тот, который мы уже разбирали:

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    

You clicked {count} times

); }


Теперь у меня к вам вопрос. Как эффект считывает самое свежее значение 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.


Компонент:

  • Вот результаты рендеринга:

    You clicked 0 times

    .
  • Кроме того, не забудь выполнить этот эффект после того, как завершишь работу: () => { document.title = 'You clicked 0 times' }.


React:

  • Конечно. Обновляю интерфейс. Эй, браузер, я добавляю кое-что в DOM.


Браузер:

  • Отлично, я вывел это на экран.


React:

  • Хорошо, теперь я запущу эффект, который мне дал компонент.
  • Выполняю () => { document.title = 'You clicked 0 times' }.


А теперь давайте разберём то, что происходит после щелчка по кнопке. На самом деле, многое тут повторяет предыдущий разбор, но кое-что здесь выглядит иначе:

Компонент:

  • Эй, React, установи моё состояние в 1.


React:

  • Дай мне пользовательский интерфейс в условиях, когда в состояние записано число 1.


Компонент:

  • Вот результаты рендеринга:

    You clicked 1 times

    .
  • Кроме того, не забудь выполнить этот эффект после того, как завершишь работу: () => { 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 (
    

You clicked {count} times

); }


Что будет выведено в консоль в том случае, если быстро щёлкнуть по кнопке несколько раз?

Как обычно, сейчас мы рассмотрим ответ на этот вопрос. Возможно, вам сейчас может показаться, что это простая задачка, и результат работы этого кода интуитивно понятен. Но это не так! Мы увидим последовательность операций, выполняющих вывод в консоль, каждая из которых принадлежит конкретному рендеру, и, в результате, пользуется собственным значением count. Попробуйте поэкспериментировать с этим примером сами.

a5727d333c270e05942f508707265378.gif


Щелчки по кнопке и вывод данных в консоль

Тут вы можете подумать: «Конечно, именно так это и работает! Да и может ли эта программа вести себя иначе?».

Ну, на самом деле, this.setState в компонентах, основанных на классах, работает не так. Поэтому легко допустить ошибку, если полагать, что следующий вариант примера, в котором используется компонент, основанный на классе, эквивалентен предыдущему:

  componentDidUpdate() {
    setTimeout(() => {
      console.log(`You clicked ${this.state.count} times`);
    }, 3000);
  }


Дело в том, что this.state.count всегда указывает на самое свежее значение count, а не на значение, принадлежащее конкретному рендеру. В результате, вместо последовательности сообщений с разными числами, мы, быстро щёлкнув по кнопке 5 раз, увидим 5 одинаковых сообщений.

264b329edc111a1973003bdf2bcacd65.gif


Щелчки по кнопке и вывод данных в консоль

Я вижу иронию в том, что хуки так сильно полагаются на 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);
  });
  // ...


78f7948263dd13b023498b23cb99f4fc.gif


Щелчки по кнопке и вывод данных в консоль

Заниматься изменениями чего-либо в 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}.

Надо отметить, что мы уже здесь были…

5fe238cf03a21dfa32af624124fcdcff.gif


А может это — та же самая кошка?

Приведём цитату из предыдущего раздела: «каждая функция внутри механизма рендеринга компонента (включая обработчики событий, эффекты, тайм-ауты или вызовы 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 (
    

Hello, {name}

); }


При его использовании совершенно неважно, будет ли сначала отрендерено , а потом — , или если компонент просто сразу выведет . И в том и в другом случаях в итоге мы увидим текст Hello, Yuzhi.

Говорят, что важен путь, а не цель. Если говорить о React, то справедливым окажется обратное утверждение. Здесь важна цель, а не то, каким путём к ней идут. В этом и заключается разница между вызовами вида $.addClass и $.removeClass в jQuery-коде (это — то, что мы называем «путём»), и указание того, каким должен быть CSS-класс в React (то есть — того, какой должна быть «цель»).

React синхронизирует DOM с тем, что имеется в текущих свойствах и состоянии. При рендеринге нет разницы между «монтированием» и «обновлением».

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

function Greeting({ name }) {
  useEffect(() => {
    document.title = 'Hello, ' + name;
  });
  return (
    

Hello, {name}

); }


В этом состоит незначительное отличие восприятия useEffect от привычной ментальной модели, в которую входят понятия монтирования, обновления и размонтирования компонентов. Если вы пытаетесь создать эффект, который ведёт себя по-особому при первом рендеринге компонента, то вы пытаетесь плыть против течения! Синхронизация не удастся в том случае, если наш результат зависит от «пути», а не от «цели».

Не должно быть разницы между тем, выполняем ли мы рендеринг компонента сначала со свойством A, потом с B, а потом — со свойством C, и той ситуацией, когда мы сразу же рендерим его со свойством C. Хотя в процессе работы этих двух вариантов кода и могут быть некоторые временные различия (например, возникающие при загрузке каких-либо данных), в итоге конечный результат должен быть тем же самым.

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

Как с этим бороться?

Учим React различать эффекты


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

© Habrahabr.ru