v19.2Latest

Разделение событий и эффектов

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

Вы узнаете
  • Как выбирать между обработчиком события и эффектом
  • Почему эффекты реактивны, а обработчики событий — нет
  • Что делать, если часть кода эффекта не должна быть реактивной
  • Что такое события эффектов и как извлечь их из эффектов
  • Как читать последние пропсы и состояние из эффектов с помощью событий эффектов

Выбор между обработчиками событий и эффектами

Сначала вспомним разницу между обработчиками событий и эффектами.

Представьте, что вы реализуете компонент чата. Ваши требования выглядят так:

  1. Ваш компонент должен автоматически подключаться к выбранной комнате чата.
  2. При нажатии кнопки «Отправить» должно отправляться сообщение в чат.

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

Обработчики событий выполняются в ответ на конкретные взаимодействия

С точки зрения пользователя, отправка сообщения должна происходитьпотому чтобыла нажата конкретная кнопка «Отправить». Пользователь будет весьма недоволен, если вы отправите его сообщение в любое другое время или по любой другой причине. Вот почему отправка сообщения должна быть обработчиком события. Обработчики событий позволяют обрабатывать конкретные взаимодействия:

С обработчиком события вы можете быть уверены, чтоsendMessage(message)выполнитсятолькоесли пользователь нажмет кнопку.

Эффекты выполняются, когда требуется синхронизация

Напомним, что вам также нужно поддерживать подключение компонента к комнате чата. Куда поместить этот код?

Причина запуска этого кода — не какое-то конкретное взаимодействие. Не важно, почему или как пользователь перешёл на экран чата. Теперь, когда он на него смотрит и может с ним взаимодействовать, компонент должен оставаться подключённым к выбранному серверу чата. Даже если компонент чата был начальным экраном вашего приложения и пользователь вообще не выполнял никаких действий, вамвсё равнонужно было бы подключиться. Вот почему это Эффект:

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

Реактивные значения и реактивная логика

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

Есть более точный способ осмыслить это.

Пропсы, состояние и переменные, объявленные в теле вашего компонента, называютсяреактивными значениями. В этом примереserverUrlне является реактивным значением, аroomId и message— являются. Они участвуют в потоке данных рендеринга:

Реактивные значения, подобные этим, могут изменяться из-за повторного рендера. Например, пользователь может отредактироватьmessageили выбрать другойroomIdв выпадающем списке. Обработчики событий и Эффекты по-разному реагируют на изменения:

  • Логика внутри обработчиков событийне является реактивной.Она не будет выполняться снова, пока пользователь не выполнит то же взаимодействие (например, клик) снова. Обработчики событий могут читать реактивные значения, не «реагируя» на их изменения.
  • Логика внутри Эффектовявляется реактивной.Если ваш Эффект читает реактивное значение,вы должны указать его как зависимость.Затем, если повторный рендер приведёт к изменению этого значения, React повторно запустит логику вашего Эффекта с новым значением.

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

Логика внутри обработчиков событий не является реактивной

Взгляните на эту строку кода. Должна ли эта логика быть реактивной или нет?

С точки зрения пользователя,изменениеmessage неозначает, что он хочет отправить сообщение.Это означает лишь то, что пользователь печатает. Другими словами, логика отправки сообщения не должна быть реактивной. Она не должна запускаться снова только потому, чтореактивное значениеизменилось. Вот почему она принадлежит обработчику события:

Обработчики событий не являются реактивными, поэтомуsendMessage(message)будет выполняться только тогда, когда пользователь нажимает кнопку «Отправить».

Логика внутри Эффектов является реактивной

Теперь вернёмся к этим строкам:

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

Эффекты являются реактивными, поэтомуcreateConnection(serverUrl, roomId) и connection.connect()будут выполняться для каждого уникального значенияroomId. Ваш Эффект синхронизирует подключение к чату с текущей выбранной комнатой.

Извлечение нереактивной логики из Эффектов

Ситуация становится сложнее, когда вы хотите смешать реактивную логику с нереактивной.

Например, представьте, что вы хотите показывать уведомление, когда пользователь подключается к чату. Вы читаете текущую тему (тёмную или светлую) из пропсов, чтобы показать уведомление в правильном цвете:

Однако,theme— это реактивное значение (оно может изменяться в результате повторного рендера), икаждое реактивное значение, прочитанное Эффектом, должно быть объявлено как его зависимость.Теперь вы должны указатьthemeкак зависимость вашего Эффекта:

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

КогдаroomIdменяется, чат переподключается, как и ожидалось. Но посколькуthemeтакже является зависимостью, чаттакжепереподключается каждый раз при переключении между тёмной и светлой темой. Это не очень хорошо!

Другими словами, выне хотите, чтобы эта строка была реактивной, даже несмотря на то, что она находится внутри эффекта (который является реактивным):

Нужен способ отделить эту нереактивную логику от окружающего её реактивного эффекта.

Объявление события эффекта

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

ЗдесьonConnectedназываетсяСобытием Эффекта.Это часть логики вашего Эффекта, но оно ведёт себя гораздо больше как обработчик события. Логика внутри него не является реактивной, и оно всегда «видит» самые последние значения ваших пропсов и состояния.

Теперь вы можете вызывать Событие ЭффектаonConnectedизнутри вашего Эффекта:

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

Убедитесь, что новое поведение работает так, как вы ожидаете:

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

Чтение последних пропсов и состояния с помощью Событий Эффектов

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

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

Позже вы добавляете на сайт несколько маршрутов. Теперь ваш компонентPageполучает пропсurlс текущим путём. Вы хотите передатьurlкак часть вашего вызоваlogVisit, но линтер зависимостей жалуется:

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

Теперь предположим, что вы хотите включать количество товаров в корзине вместе с каждым посещением страницы:

Вы использовалиnumberOfItemsвнутри эффекта, поэтому линтер просит вас добавить его в зависимости. Однако выне хотите, чтобы вызовlogVisitбыл реактивным относительноnumberOfItems. Если пользователь положит что-то в корзину иnumberOfItemsизменится, этоне означает, что пользователь снова посетил страницу. Другими словами,посещение страницы— это, в некотором смысле, «событие». Оно происходит в определённый момент времени.

Разделите код на две части:

ЗдесьonVisit— это событие эффекта. Код внутри него не является реактивным. Поэтому вы можете использоватьnumberOfItems(или любое другое реактивное значение!), не беспокоясь, что это приведёт к повторному выполнению окружающего кода при изменениях.

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

В результате вы будете вызыватьlogVisitпри каждом измененииurlи всегда читать последнее значениеnumberOfItems. Однако еслиnumberOfItemsизменится сам по себе, это не приведёт к повторному выполнению какого-либо кода.

Примечание

Возможно, вам интересно, можно ли вызватьonVisit()без аргументов и прочитатьurlвнутри него:

Это сработает, но лучше явно передать этотurlв событие эффекта.Передаваяurlв качестве аргумента вашему событию эффекта, вы указываете, что посещение страницы с другимurlс точки зрения пользователя является отдельным «событием». visitedUrl— эточастьпроизошедшего «события»:

Поскольку ваше событие эффекта явно «запрашивает»visitedUrl, теперь вы не сможете случайно удалитьurlиз зависимостей эффекта. Если вы удалите зависимостьurl(из-за чего разные посещения страниц будут считаться одним), линтер предупредит вас об этом. Вы хотите, чтобыonVisitбыл реактивным относительноurl, поэтому вместо чтенияurlвнутри (где он не был бы реактивным), вы передаёте егоизвашего эффекта.

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

ЗдесьurlвнутриonVisitсоответствуетпоследнемуurl(который уже мог измениться), аvisitedUrlсоответствует томуurl, который изначально вызвал запуск этого эффекта (и этого вызова

Ограничения событий эффектов

События эффектов имеют серьёзные ограничения в использовании:

  • Вызывайте их только внутри эффектов.
  • Никогда не передавайте их другим компонентам или хукам.

Например, не объявляйте и не передавайте событие эффекта так:

Вместо этого всегда объявляйте события эффектов непосредственно рядом с эффектами, которые их используют:

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

Итоги

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

Try out some challenges

Challenge 1 of 4:Fix a variable that doesn’t update #

This Timer component keeps a count state variable which increases every second. The value by which it’s increasing is stored in the increment state variable. You can control the increment variable with the plus and minus buttons.

However, no matter how many times you click the plus button, the counter is still incremented by one every second. What’s wrong with this code? Why is increment always equal to 1 inside the Effect’s code? Find the mistake and fix it.