Жизненный цикл реактивных эффектов
Эффекты имеют жизненный цикл, отличный от компонентов. Компоненты могут монтироваться, обновляться или размонтироваться. Эффект может делать только две вещи: начать синхронизацию чего-либо и позже остановить её. Этот цикл может повторяться много раз, если ваш эффект зависит от пропсов и состояния, которые меняются со временем. React предоставляет правило линтера для проверки правильности указания зависимостей эффекта. Это позволяет эффекту оставаться синхронизированным с последними пропсами и состоянием.
Вы узнаете
- Чем жизненный цикл эффекта отличается от жизненного цикла компонента
- Как рассматривать каждый эффект изолированно
- Когда вашему эффекту требуется повторная синхронизация и почему
- Как определяются зависимости вашего эффекта
- Что значит, что значение является реактивным
- Что означает пустой массив зависимостей
- Как React проверяет правильность ваших зависимостей с помощью линтера
- Что делать, если вы не согласны с линтером
Жизненный цикл эффекта
Каждый компонент React проходит один и тот же жизненный цикл:
- Компонентмонтируется, когда он добавляется на экран.
- Компонентобновляется, когда он получает новые пропсы или состояние, обычно в ответ на взаимодействие.
- Компонентразмонтируется, когда он удаляется с экрана.
Это хороший способ думать о компонентах, нонеоб эффектах.Вместо этого попробуйте думать о каждом эффекте независимо от жизненного цикла вашего компонента. Эффект описывает, каксинхронизировать внешнюю системус текущими пропсами и состоянием. По мере изменения вашего кода синхронизация может требоваться чаще или реже.
Чтобы проиллюстрировать это, рассмотрим этот эффект, подключающий ваш компонент к серверу чата:
Тело вашего эффекта определяет, какначать синхронизацию:
Функция очистки, возвращаемая вашим эффектом, определяет, какостановить синхронизацию:
Интуитивно можно подумать, что React будетначинать синхронизациюпри монтировании компонента иостанавливать синхронизациюпри его размонтировании. Однако это ещё не всё! Иногда также может потребоватьсянесколько раз начинать и останавливать синхронизацию, пока компонент остаётся смонтированным.
Давайте рассмотрим,почемуэто необходимо,когдаэто происходит икаквы можете управлять этим поведением.
Примечание
Некоторые эффекты вообще не возвращают функцию очистки.Чаще всеговы захотите её вернуть, но если вы этого не сделаете, React будет вести себя так, как если бы вы вернули пустую функцию очистки.
Почему синхронизация может требоваться более одного раза
Представьте, что этот компонентChatRoomполучает пропсroomId, который пользователь выбирает в выпадающем списке. Допустим, изначально пользователь выбирает комнату"general"в качествеroomId. Ваше приложение отображает чат-комнату"general":
После отображения интерфейса React запустит ваш эффект, чтобыначать синхронизацию.Он подключится к комнате"general":
Пока всё идёт хорошо.
Позже пользователь выбирает другую комнату в выпадающем списке (например,"travel"). Сначала React обновит интерфейс:
Подумайте, что должно произойти дальше. Пользователь видит, что"travel"— выбранная чат-комната в интерфейсе. Однако Эффект, который выполнялся в прошлый раз, всё ещё подключён к комнате"general". ПропсroomIdизменился, поэтому то, что делал ваш Эффект ранее (подключение к комнате"general"), больше не соответствует интерфейсу.
На этом этапе вы хотите, чтобы React сделал две вещи:
- Прекратить синхронизацию со старым
roomId(отключиться от комнаты"general") - Начать синхронизацию с новым
roomId(подключиться к комнате"travel")
К счастью, вы уже научили React делать и то, и другое!Тело вашего Эффекта определяет, как начать синхронизацию, а функция очистки определяет, как её остановить. Теперь React нужно лишь вызвать их в правильном порядке с правильными пропсами и состоянием. Давайте посмотрим, как именно это происходит.
Как React повторно синхронизирует ваш Эффект
Напомним, что ваш компонентChatRoomполучил новое значение для пропсаroomId. Раньше это было"general", а теперь это"travel". React необходимо повторно синхронизировать ваш Эффект, чтобы переподключить вас к другой комнате.
Чтобыостановить синхронизацию,React вызовет функцию очистки, которую вернул ваш Эффект после подключения к комнате"general". ПосколькуroomIdбыл"general", функция очистки отключается от комнаты"general":
Затем React выполнит Эффект, который вы предоставили во время этого рендера. На этот разroomId— это"travel", поэтому онначнёт синхронизациюс чат-комнатой"travel"(пока её функция очистки тоже не будет вызвана в конечном итоге):
Благодаря этому вы теперь подключены к той же комнате, которую выбрал пользователь в интерфейсе. Катастрофа предотвращена!
Каждый раз после повторного рендера вашего компонента с другимroomIdваш Эффект будет повторно синхронизироваться. Например, предположим, пользователь меняетroomId с "travel"на"music". React сноваостановит синхронизациювашего Эффекта, вызвав его функцию очистки (отключив вас от комнаты"travel"). Затем онснова начнёт синхронизацию, выполнив его тело с новым пропсомroomId(подключив вас к комнате"music").
Наконец, когда пользователь переходит на другой экран, компонентChatRoomразмонтируется. Теперь вообще нет необходимости оставаться подключённым. Reactостановит синхронизациювашего Эффекта в последний раз и отключит вас от чат-комнаты"music".
Мышление с точки зрения Эффекта
Давайте подытожим всё, что произошло с точки зрения компонентаChatRoom:
ChatRoomсмонтирован сroomIdравным"general"ChatRoomобновлён сroomIdравным"travel"ChatRoomобновлён сroomIdравным"music"ChatRoomразмонтирован
В каждой из этих точек жизненного цикла компонента ваш Эффект выполнял разные действия:
- Ваш Эффект подключился к комнате
"general" - Ваш Эффект отключился от комнаты
"general"и подключился к комнате"travel" - Ваш Эффект отключился от комнаты
"travel"и подключился к комнате"music" - Ваш Эффект отключился от комнаты
"music"
Теперь давайте подумаем о том, что происходило с точки зрения самого Эффекта:
Структура этого кода может натолкнуть вас на мысль о том, что произошло как о последовательности непересекающихся временных периодов:
- Ваш эффект подключился к комнате
"general"(до отключения) - Ваш эффект подключился к комнате
"travel"(до отключения) - Ваш эффект подключился к комнате
"music"(до отключения)
Ранее вы мыслили с точки зрения компонента. Когда вы смотрели с этой точки зрения, было соблазнительно думать об эффектах как о «колбэках» или «событиях жизненного цикла», которые срабатывают в определённое время, например «после рендера» или «перед размонтированием». Такой подход очень быстро усложняется, поэтому его лучше избегать.
Вместо этого всегда сосредотачивайтесь на одном цикле запуска/остановки за раз. Не должно иметь значения, монтируется ли компонент, обновляется или размонтируется. Всё, что вам нужно сделать, — это описать, как начать синхронизацию и как её остановить. Если вы сделаете это хорошо, ваш эффект будет устойчив к запуску и остановке столько раз, сколько потребуется.
Это может напомнить вам, как вы не задумываетесь о том, монтируется компонент или обновляется, когда пишете логику рендеринга, создающую JSX. Вы описываете, что должно быть на экране, а Reactразбирается с остальным.
Как React проверяет, что ваш эффект можно повторно синхронизировать
Вот живой пример, с которым можно поиграть. Нажмите «Открыть чат», чтобы смонтировать компонентChatRoom:
Обратите внимание, что при первом монтировании компонента вы видите три записи в логе:
✅ Connecting to "general" room at https://localhost:1234...(только в разработке)❌ Disconnected from "general" room at https://localhost:1234.(только в разработке)✅ Connecting to "general" room at https://localhost:1234...
Первые два сообщения журнала предназначены только для разработки. В режиме разработки React всегда повторно монтирует каждый компонент один раз.
React проверяет, может ли ваш Эффект повторно синхронизироваться, принудительно выполняя это сразу в режиме разработки.Это может напомнить вам открытие и закрытие двери лишний раз, чтобы проверить, работает ли дверной замок. React запускает и останавливает ваш Эффект один дополнительный раз в разработке, чтобы проверить,хорошо ли вы реализовали его очистку.
Основная причина, по которой ваш Эффект будет повторно синхронизироваться на практике, — это изменение некоторых используемых им данных. В приведённой выше песочнице измените выбранный чат. Обратите внимание, как при измененииroomIdваш Эффект повторно синхронизируется.
Однако существуют и более необычные случаи, когда повторная синхронизация необходима. Например, попробуйте отредактироватьserverUrlв песочнице выше, пока чат открыт. Обратите внимание, как Эффект повторно синхронизируется в ответ на ваши правки кода. В будущем React может добавить больше функций, которые полагаются на повторную синхронизацию.
Как React узнаёт, что нужно повторно синхронизировать Эффект
Вам может быть интересно, как React узнал, что ваш Эффект нужно повторно синхронизировать после измененияroomId. Это потому, чтовы сообщили React, что его код зависит отroomId, включив его всписок зависимостей:
Вот как это работает:
- Вы знали, что
roomId— это пропс, который может меняться со временем. - Вы знали, что ваш Эффект читает
roomId(поэтому его логика зависит от значения, которое может измениться позже). - Вот почему вы указали его как зависимость вашего Эффекта (чтобы он повторно синхронизировался при изменении
roomId).
Каждый раз после повторного рендеринга вашего компонента React будет смотреть на массив зависимостей, который вы передали. Если какое-либо значение в массиве отличается от значения на той же позиции, которое вы передали во время предыдущего рендера, React повторно синхронизирует ваш Эффект.
Например, если вы передали["general"]во время начального рендера, а позже передали["travel"]во время следующего рендера, React сравнит"general" и "travel". Это разные значения (сравниваются с помощьюObject.is), поэтому React повторно синхронизирует ваш Эффект. С другой стороны, если ваш компонент перерендеривается, ноroomIdне изменился, ваш Эффект останется подключённым к той же комнате.
Каждый Эффект представляет отдельный процесс синхронизации
Не поддавайтесь желанию добавлять несвязанную логику в ваш Эффект только потому, что эта логика должна выполняться одновременно с уже написанным Эффектом. Например, предположим, вы хотите отправить аналитическое событие, когда пользователь посещает комнату. У вас уже есть Эффект, который зависит отroomId, поэтому вам может захотеться добавить вызов аналитики туда:
Но представьте, что позже вы добавляете в этот Эффект ещё одну зависимость, которая требует повторного установления соединения. Если этот Эффект повторно синхронизируется, он также вызоветlogVisit(roomId)для той же комнаты, чего вы не планировали. Логирование посещения —это отдельный процессот подключения. Напишите их как два отдельных Эффекта:
Каждый Эффект в вашем коде должен представлять отдельный и независимый процесс синхронизации.
В приведённом выше примере удаление одного Эффекта не нарушит логику другого Эффекта. Это хороший признак того, что они синхронизируют разные вещи, и поэтому имело смысл разделить их. С другой стороны, если вы разделите целостный фрагмент логики на отдельные Эффекты, код может выглядеть «чище», но будетсложнее в поддержке.Вот почему вам следует думать о том, являются ли процессы одинаковыми или отдельными, а не о том, выглядит ли код чище.
Эффекты «реагируют» на реактивные значения
Ваш Эффект читает две переменные (serverUrl и roomId), но вы указали толькоroomIdв качестве зависимости:
ПочемуserverUrlне нужно указывать в зависимостях?
Это потому, чтоserverUrlникогда не меняется из-за повторного рендера. Он всегда один и тот же, независимо от того, сколько раз компонент перерисовывается и почему. ПосколькуserverUrlникогда не меняется, нет смысла указывать его в зависимостях. В конце концов, зависимости что-то делают только тогда, когда они меняются со временем!
С другой стороны,roomIdможет быть другим при повторном рендере.Пропсы, состояние и другие значения, объявленные внутри компонента, являютсяреактивными, потому что они вычисляются во время рендеринга и участвуют в потоке данных React.
Если быserverUrlбыл переменной состояния, он был бы реактивным. Реактивные значения должны быть включены в зависимости:
ВключивserverUrlв зависимости, вы гарантируете, что эффект повторно синхронизируется после его изменения.
Попробуйте изменить выбранный чат или отредактировать URL сервера в этой песочнице:
Всякий раз, когда вы меняете реактивное значение, такое какroomIdилиserverUrl, эффект повторно подключается к серверу чата.
Что означает эффект с пустыми зависимостями
Что произойдет, если вы вынесете иserverUrl, иroomIdза пределы компонента?
Теперь код вашего эффекта не используетникакихреактивных значений, поэтому его зависимости могут быть пустыми ([]).
Если рассуждать с точки зрения компонента, пустой массив зависимостей[]означает, что этот эффект подключается к чату только при монтировании компонента и отключается только при размонтировании. (Помните, что в режиме разработки React всё равноповторно синхронизирует его один дополнительный раз, чтобы протестировать вашу логику.)
Однако если вырассуждаете с точки зрения эффекта,вам вообще не нужно думать о монтировании и размонтировании. Важно то, что вы указали, что делает ваш эффект для начала и остановки синхронизации. Сегодня у него нет реактивных зависимостей. Но если вы когда-нибудь захотите, чтобы пользователь мог менятьroomIdилиserverUrlсо временем (и они станут реактивными), код вашего эффекта не изменится. Вам нужно будет только добавить их в зависимости.
Все переменные, объявленные в теле компонента, являются реактивными
Пропсы и состояние — не единственные реактивные значения. Значения, вычисленные из них, также являются реактивными. Если пропсы или состояние изменятся, ваш компонент перерендерится, и значения, вычисленные из них, также изменятся. Вот почему все переменные из тела компонента, используемые эффектом, должны быть в списке зависимостей эффекта.
Допустим, пользователь может выбрать сервер чата в выпадающем списке, но также может настроить сервер по умолчанию в настройках. Предположим, вы уже поместили состояние настроек вконтекст, поэтому вы читаетеsettingsиз этого контекста. Теперь вы вычисляетеserverUrlна основе выбранного сервера из пропсов и сервера по умолчанию:
В этом примереserverUrlне является пропсом или переменной состояния. Это обычная переменная, которую вы вычисляете во время рендеринга. Но она вычисляется во время рендеринга, поэтому может измениться из-за повторного рендеринга. Вот почему она реактивна.
Все значения внутри компонента (включая пропсы, состояние и переменные в теле компонента) являются реактивными. Любое реактивное значение может измениться при повторном рендеринге, поэтому вам нужно включать реактивные значения в зависимости эффекта.
Другими словами, эффекты «реагируют» на все значения из тела компонента.
React проверяет, что вы указали каждое реактивное значение как зависимость
Если ваш линтернастроен для React,он будет проверять, что каждое реактивное значение, используемое в коде вашего эффекта, объявлено как его зависимость. Например, это ошибка линтера, потому что иroomId, иserverUrlявляются реактивными:
Это может выглядеть как ошибка React, но на самом деле React указывает на ошибку в вашем коде. ИroomId, иserverUrlмогут меняться со временем, но вы забываете повторно синхронизировать ваш эффект, когда они меняются. Вы останетесь подключенным к начальнымroomId и serverUrl, даже после того как пользователь выберет другие значения в интерфейсе.
Чтобы исправить ошибку, следуйте предложению линтера и укажитеroomId и serverUrlкак зависимости вашего эффекта:
Попробуйте это исправление в песочнице выше. Убедитесь, что ошибка линтера исчезла, и чат переподключается, когда это необходимо.
Примечание
В некоторых случаях Reactзнает, что значение никогда не меняется, даже если оно объявлено внутри компонента. Например, функцияset, возвращаемая изuseState, и объект ref, возвращаемыйuseRef, являютсястабильными— они гарантированно не меняются при повторном рендеринге. Стабильные значения не являются реактивными, поэтому вы можете опустить их из списка. Их включение допустимо: они не изменятся, поэтому это не имеет значения.
Что делать, если вы не хотите повторно синхронизировать
В предыдущем примере вы исправили ошибку линтера, перечисливroomId и serverUrlкак зависимости.
Однако вы можете вместо этого «доказать» линтеру, что эти значения не являются реактивными,т.е. что онине могутизмениться в результате повторного рендеринга. Например, еслиserverUrl и roomIdне зависят от рендеринга и всегда имеют одни и те же значения, вы можете вынести их за пределы компонента. Теперь они не нуждаются в зависимостях:
Вы также можете переместить ихвнутрь эффекта.Они не вычисляются во время рендеринга, поэтому не являются реактивными:
Эффекты — это реактивные блоки кода.Они повторно синхронизируются, когда изменяются значения, которые вы читаете внутри них. В отличие от обработчиков событий, которые выполняются один раз за взаимодействие, эффекты выполняются всякий раз, когда синхронизация необходима.
Вы не можете «выбирать» свои зависимости.Ваши зависимости должны включать каждоереактивное значение, которое вы читаете в эффекте. Линтер обеспечивает это. Иногда это может привести к таким проблемам, как бесконечные циклы и слишком частая повторная синхронизация вашего эффекта. Не исправляйте эти проблемы, подавляя линтер! Вот что можно попробовать вместо этого:
- Убедитесь, что ваш эффект представляет собой независимый процесс синхронизации.Если ваш эффект ничего не синхронизирует,он может быть не нужен.Если он синхронизирует несколько независимых вещей,разделите его.
- Если вы хотите прочитать последнее значение пропсов или состояния, не «реагируя» на него и не вызывая повторную синхронизацию эффекта,вы можете разделить ваш эффект на реактивную часть (которую вы оставите в эффекте) и нереактивную часть (которую вы извлечёте во что-то, называемоесобытием эффекта).Прочитайте о разделении событий и эффектов.
- Избегайте использования объектов и функций в качестве зависимостей.Если вы создаёте объекты и функции во время рендеринга, а затем читаете их из эффекта, они будут разными при каждом рендере. Это приведёт к повторной синхронизации вашего эффекта каждый раз.Подробнее об удалении ненужных зависимостей из эффектов.
Подводный камень
Линтер — ваш друг, но его возможности ограничены. Линтер знает только, когда зависимостинеправильные. Он не знаетлучшийспособ решения каждого случая. Если линтер предлагает добавить зависимость, но её добавление вызывает цикл, это не значит, что линтер следует игнорировать. Вам нужно изменить код внутри (или снаружи) эффекта так, чтобы это значение не было реактивным и нетребовалобыть зависимостью.
Если у вас есть существующая кодовая база, в ней могут быть эффекты, которые подавляют линтер, например так:
На следующихстраницахвы узнаете, как исправить этот код, не нарушая правил. Это всегда стоит того!
Резюме
- Компоненты могут монтироваться, обновляться и размонтироваться.
- Каждый эффект имеет отдельный жизненный цикл от окружающего компонента.
- Каждый эффект описывает отдельный процесс синхронизации, который можнозапустить и остановить.
- Когда вы пишете и читаете эффекты, думайте с точки зрения каждого отдельного эффекта (как запустить и остановить синхронизацию), а не с точки зрения компонента (как он монтируется, обновляется или размонтируется).
- Значения, объявленные в теле компонента, являются «реактивными».
- Реактивные значения должны вызывать повторную синхронизацию эффекта, потому что они могут меняться со временем.
- Линтер проверяет, что все реактивные значения, используемые внутри эффекта, указаны как зависимости.
- Все ошибки, отмеченные линтером, являются обоснованными. Всегда есть способ исправить код, не нарушая правил.
Try out some challenges
Challenge 1 of 5:Fix reconnecting on every keystroke #
In this example, the ChatRoom component connects to the chat room when the component mounts, disconnects when it unmounts, and reconnects when you select a different chat room. This behavior is correct, so you need to keep it working.
However, there is a problem. Whenever you type into the message box input at the bottom, ChatRoom also reconnects to the chat. (You can notice this by clearing the console and typing into the input.) Fix the issue so that this doesn’t happen.
