Удаление зависимостей эффекта
Когда вы пишете эффект, линтер проверяет, что вы включили в список зависимостей эффекта каждое реактивное значение (например, пропсы и состояние), которое эффект считывает. Это гарантирует, что ваш эффект остаётся синхронизированным с последними пропсами и состоянием вашего компонента. Лишние зависимости могут привести к слишком частому запуску эффекта или даже к созданию бесконечного цикла. Следуйте этому руководству, чтобы проверить и удалить ненужные зависимости из ваших эффектов.
Вы узнаете
- Как исправить бесконечные циклы зависимостей эффекта
- Что делать, если вы хотите удалить зависимость
- Как считывать значение из эффекта, не «реагируя» на него
- Как и почему избегать зависимостей от объектов и функций
- Почему подавление линтера зависимостей опасно и что делать вместо этого
Зависимости должны соответствовать коду
Когда вы пишете эффект, вы сначала указываете, какзапустить и остановитьто, что должен делать ваш эффект:
Затем, если вы оставите зависимости эффекта пустыми ([]), линтер предложит правильные зависимости:
Заполните их в соответствии с указаниями линтера:
Эффекты «реагируют» на реактивные значения.ПосколькуroomId— это реактивное значение (оно может измениться при повторном рендере), линтер проверяет, что вы указали его как зависимость. ЕслиroomIdполучит другое значение, React повторно синхронизирует ваш эффект. Это гарантирует, что чат остаётся подключённым к выбранной комнате и «реагирует» на выбор в выпадающем списке:
Чтобы удалить зависимость, докажите, что она не является зависимостью
Обратите внимание, что вы не можете «выбирать» зависимости вашего эффекта. Каждоереактивное значение, используемое в коде вашего эффекта, должно быть объявлено в списке зависимостей. Список зависимостей определяется окружающим кодом:
Реактивные значениявключают пропсы и все переменные и функции, объявленные непосредственно внутри вашего компонента. ПосколькуroomIdявляется реактивным значением, вы не можете удалить его из списка зависимостей. Линтер не позволит это сделать:
И линтер будет прав! ПосколькуroomIdможет меняться со временем, это приведёт к ошибке в вашем коде.
Чтобы удалить зависимость, «докажите» линтеру, что онане должнабыть зависимостью.Например, вы можете вынестиroomIdиз вашего компонента, чтобы доказать, что оно не является реактивным и не будет меняться при повторных рендерах:
Теперь, когдаroomIdне является реактивным значением (и не может меняться при повторном рендере), оно не должно быть зависимостью:
Вот почему теперь вы можете указатьпустой ([]) список зависимостей.Ваш эффектдействительно больше независит ни от каких реактивных значений, поэтому емудействительно не нужноповторно запускаться при изменении любых пропсов или состояния компонента.
Чтобы изменить зависимости, измените код
Возможно, вы заметили закономерность в своей работе:
- Сначала выизменяете кодсвоего эффекта или то, как объявлены ваши реактивные значения.
- Затем вы следуете указаниям линтера и настраиваете зависимости, чтобы онисоответствовали изменённому вами коду.
- Если вас не устраивает список зависимостей, вывозвращаетесь к первому шагу(и снова изменяете код).
Последняя часть важна.Если вы хотите изменить зависимости, сначала измените окружающий код.Вы можете думать о списке зависимостей как осписке всех реактивных значений, используемых в коде вашего эффекта.Вы невыбираете, что поместить в этот список. Списокописываетваш код. Чтобы изменить список зависимостей, измените код.
Это может напоминать решение уравнения. Вы можете начать с цели (например, удалить зависимость), и вам нужно «найти» код, соответствующий этой цели. Не всем нравится решать уравнения, и то же самое можно сказать о написании эффектов! К счастью, ниже есть список распространённых рецептов, которые вы можете попробовать.
Подводный камень
Если у вас есть существующая кодовая база, в ней могут быть эффекты, которые подавляют линтер следующим образом:
Когда зависимости не соответствуют коду, существует очень высокий риск появления ошибок.Подавляя линтер, вы «лжёте» React о значениях, от которых зависит ваш эффект.
Вместо этого используйте приведённые ниже техники.
Удаление ненужных зависимостей
Каждый раз, когда вы настраиваете зависимости эффекта, чтобы они отражали код, посмотрите на список зависимостей. Имеет ли смысл повторный запуск эффекта при изменении любой из этих зависимостей? Иногда ответ — «нет»:
- Возможно, вам потребуется повторно выполнятьразные частивашего эффекта при разных условиях.
- Возможно, вы захотите читать толькопоследнее значениекакой-либо зависимости, а не «реагировать» на её изменения.
- Зависимость может меняться слишком частонепреднамеренно, потому что она является объектом или функцией.
Чтобы найти правильное решение, вам нужно ответить на несколько вопросов о вашем эффекте. Давайте рассмотрим их.
Следует ли перенести этот код в обработчик события?
Первое, о чём вам стоит подумать, — должен ли этот код вообще быть эффектом.
Представьте себе форму. При отправке вы устанавливаете переменную состоянияsubmittedв значениеtrue. Вам нужно отправить POST-запрос и показать уведомление. Вы поместили эту логику внутрь эффекта, который «реагирует» на то, чтоsubmittedравноtrue:
Позже вы хотите стилизовать сообщение уведомления в соответствии с текущей темой, поэтому вы читаете текущую тему. Посколькуthemeобъявлена в теле компонента, она является реактивным значением, поэтому вы добавляете её в зависимости:
Сделав это, вы внесли ошибку. Представьте, что вы сначала отправляете форму, а затем переключаетесь между тёмной и светлой темами.themeизменится, эффект выполнится повторно, и поэтому он снова покажет то же самое уведомление!
Проблема здесь в том, что это вообще не должно было быть эффектом.Вы хотите отправить этот POST-запрос и показать уведомление в ответ наотправку формы,что является конкретным взаимодействием. Чтобы выполнить какой-либо код в ответ на конкретное взаимодействие, поместите эту логику непосредственно в соответствующий обработчик события:
Теперь, когда код находится в обработчике события, он не является реактивным — поэтому он будет выполняться только тогда, когда пользователь отправляет форму. Подробнее овыборе между обработчиками событий и эффектамии о том,как удалить ненужные эффекты.
Выполняет ли ваш эффект несколько несвязанных действий?
Следующий вопрос, который вы должны себе задать, — выполняет ли ваш эффект несколько несвязанных действий.
Представьте, что вы создаёте форму доставки, где пользователю нужно выбрать свой город и район. Вы получаете списокcitiesс сервера в соответствии с выбраннойcountry, чтобы показать их в выпадающем списке:
Это хороший примерполучения данных в эффекте.Вы синхронизируете состояниеcitiesс сетью в соответствии со свойствомcountry. Вы не можете сделать это в обработчике события, потому что вам нужно получить данные, как толькоShippingFormотобразится, и всякий раз, когдаcountryизменится (независимо от того, какое взаимодействие это вызвало).
Теперь предположим, что вы добавляете второй выпадающий список для районов города, который должен получатьareasдля текущего выбранногоcity. Вы можете начать с добавления второго вызоваfetchдля списка районов внутри того же эффекта:
Однако, поскольку эффект теперь использует переменную состоянияcity, вам пришлось добавитьcityв список зависимостей. Это, в свою очередь, создало проблему: когда пользователь выбирает другой город, эффект выполнится повторно и вызоветfetchCities(country). В результате вы будете без необходимости многократно повторно загружать список городов.
Проблема этого кода в том, что вы синхронизируете две разные, не связанные между собой вещи:
- Вы хотите синхронизировать состояние
citiesс сетью на основе пропсаcountry. - Вы хотите синхронизировать состояние
areasс сетью на основе состоянияcity.
Разделите логику на два эффекта, каждый из которых реагирует на проп, с которым ему нужно синхронизироваться:
Теперь первый эффект повторно выполняется только при измененииcountry, а второй эффект — при измененииcity. Вы разделили их по назначению: две разные вещи синхронизируются двумя отдельными эффектами. Два отдельных эффекта имеют два отдельных списка зависимостей, поэтому они не будут непреднамеренно запускать друг друга.
Итоговый код длиннее исходного, но разделение этих эффектов всё равно правильно.Каждый эффект должен представлять собой независимый процесс синхронизации.В этом примере удаление одного эффекта не нарушает логику другого эффекта. Это означает, что онисинхронизируют разные вещи,и их хорошо разделить. Если вас беспокоит дублирование, вы можете улучшить этот код,вынеся повторяющуюся логику в пользовательский хук.
Вы читаете какое-то состояние для вычисления следующего состояния?
Этот эффект обновляет переменную состоянияmessagesновым созданным массивом каждый раз, когда приходит новое сообщение:
Он использует переменнуюmessages для создания нового массива, начинающегося со всех существующих сообщений, и добавляет новое сообщение в конец. Однако, посколькуmessages— это реактивное значение, читаемое эффектом, оно должно быть зависимостью:
И добавлениеmessagesв зависимости создаёт проблему.
Каждый раз, когда вы получаете сообщение,setMessages()вызывает повторный рендер компонента с новым массивомmessages, который включает полученное сообщение. Однако, поскольку этот эффект теперь зависит отmessages, этотакжеприведёт к повторной синхронизации эффекта. Таким образом, каждое новое сообщение заставит чат переподключаться. Пользователю это не понравится!
Чтобы исправить проблему, не читайтеmessagesвнутри эффекта. Вместо этого передайтефункцию-обновитель в setMessages:
Обратите внимание, что ваш эффект теперь вообще не читает переменнуюmessages.Вам нужно лишь передать функцию-обновление, напримерmsgs => [...msgs, receivedMessage]. Reactставит вашу функцию-обновление в очередьи предоставит аргументmsgsво время следующего рендера. Вот почему сам эффект больше не должен зависеть отmessages. В результате этого исправления получение сообщения в чате больше не будет вызывать повторное подключение чата.
Хотите прочитать значение, не «реагируя» на его изменения?
Предположим, вы хотите воспроизводить звук, когда пользователь получает новое сообщение, если толькоisMutedне равноtrue:
Поскольку ваш эффект теперь используетisMutedв своём коде, вы должны добавить его в зависимости:
Проблема в том, что каждый раз, когдаisMutedизменяется (например, когда пользователь нажимает переключатель «Без звука»), эффект будет повторно синхронизироваться и переподключаться к чату. Это не тот пользовательский опыт, который нужен! (В этом примере даже отключение линтера не поможет — если вы это сделаете,isMuted«застрянет» со своим старым значением.)
Чтобы решить эту проблему, вам нужно извлечь логику, которая не должна быть реактивной, из эффекта. Вы не хотите, чтобы этот эффект «реагировал» на измененияisMuted.Переместите эту нереактивную часть логики в событие эффекта:
События эффекта позволяют разделить эффект на реактивные части (которые должны «реагировать» на реактивные значения, такие какroomIdи их изменения) и нереактивные части (которые только читают их последние значения, какonMessageчитаетisMuted).Теперь, когда вы читаетеisMutedвнутри события эффекта, оно не должно быть зависимостью вашего эффекта.В результате чат не будет переподключаться при включении и выключении настройки «Без звука», что решает исходную проблему!
Оборачивание обработчика событий из пропсов
Вы можете столкнуться с похожей проблемой, когда ваш компонент получает обработчик событий в качестве пропса:
Предположим, что родительский компонент передаётразнуюonReceiveMessageфункцию при каждом рендере:
ПосколькуonReceiveMessageявляется зависимостью, это приведёт к повторной синхронизации эффекта после каждого перерендера родителя. Это заставит его переподключаться к чату. Чтобы решить эту проблему, оберните вызов в событие эффекта:
События эффекта не являются реактивными, поэтому вам не нужно указывать их в зависимостях. В результате чат больше не будет переподключаться, даже если родительский компонент передаёт функцию, которая отличается при каждом перерендере.
Разделение реактивного и нереактивного кода
В этом примере вы хотите логировать посещение каждый раз, когда изменяетсяroomId. Вы хотите включать текущее значениеnotificationCountв каждую запись лога, но выне хотите, чтобы изменениеnotificationCountвызывало событие логирования.
Решение снова заключается в выделении нереактивного кода в событие эффекта:
Вы хотите, чтобы ваша логика реагировала наroomId, поэтому вы читаетеroomIdвнутри вашего эффекта. Однако вы не хотите, чтобы изменениеnotificationCountприводило к дополнительной записи о посещении, поэтому вы читаетеnotificationCountвнутри события эффекта.Узнайте больше о чтении последних пропсов и состояния из эффектов с помощью событий эффекта.
Изменяется ли какое-то реактивное значение непреднамеренно?
Иногда выдействительнохотите, чтобы ваш эффект «реагировал» на определённое значение, но это значение меняется чаще, чем вам хотелось бы, и может не отражать реальных изменений с точки зрения пользователя. Например, предположим, что вы создаёте объектoptionsв теле вашего компонента, а затем читаете этот объект внутри вашего эффекта:
Этот объект объявлен в теле компонента, поэтому он являетсяреактивным значением.Когда вы читаете такое реактивное значение внутри эффекта, вы объявляете его как зависимость. Это гарантирует, что ваш эффект будет «реагировать» на его изменения:
Важно объявить его как зависимость! Это гарантирует, например, что еслиroomIdизменится, ваш эффект переподключится к чату с новымиoptions. Однако в приведённом выше коде также есть проблема. Чтобы увидеть её, попробуйте ввести текст в поле ввода в песочнице ниже и посмотрите, что произойдёт в консоли:
В песочнице выше поле ввода обновляет только переменную состоянияmessage. С точки зрения пользователя, это не должно влиять на подключение к чату. Однако каждый раз, когда вы обновляетеmessage, ваш компонент перерисовывается. Когда ваш компонент перерисовывается, код внутри него выполняется заново с нуля.
Новый объектoptionsсоздаётся с нуля при каждой перерисовке компонентаChatRoom. React видит, что объектoptions— этодругой объект, отличный от объектаoptions, созданного во время последней отрисовки. Вот почему он повторно синхронизирует ваш эффект (который зависит отoptions), и чат переподключается по мере ввода текста.
Эта проблема затрагивает только объекты и функции. В JavaScript каждый вновь созданный объект и функция считаются отличными от всех остальных. Неважно, что их содержимое может быть одинаковым!
Зависимости в виде объектов и функций могут вызывать повторную синхронизацию вашего эффекта чаще, чем необходимо.
Поэтому, по возможности, следует избегать использования объектов и функций в качестве зависимостей эффекта. Вместо этого попробуйте вынести их за пределы компонента, внутрь эффекта или извлечь из них примитивные значения.
Вынесение статических объектов и функций за пределы компонента
Если объект не зависит от пропсов и состояния, его можно вынести за пределы компонента:
Таким образом, выдоказываетелинтеру, что это значение не реактивное. Оно не может измениться в результате повторного рендера, поэтому не должно быть зависимостью. Теперь повторный рендерChatRoomне вызовет повторную синхронизацию вашего эффекта.
Это также работает и для функций:
ПосколькуcreateOptionsобъявлена вне вашего компонента, она не является реактивным значением. Поэтому её не нужно указывать в зависимостях эффекта, и она никогда не вызовет его повторную синхронизацию.
Помещение динамических объектов и функций внутрь эффекта
Если ваш объект зависит от какого-либо реактивного значения, которое может измениться в результате повторного рендера, например, пропсаroomId, вы не можете вынести егоза пределыкомпонента. Однако вы можете перенести его созданиевнутрькода вашего эффекта:
Теперь, когдаoptionsобъявлен внутри вашего эффекта, он больше не является зависимостью эффекта. Вместо этого единственным реактивным значением, используемым эффектом, являетсяroomId. ПосколькуroomId— это не объект и не функция, вы можете быть уверены, что он не будетслучайноотличаться. В JavaScript числа и строки сравниваются по их содержимому:
Благодаря этому исправлению чат больше не переподключается при редактировании поля ввода:
Однако, ондействительнопереподключается, когда вы меняете значение в выпадающем спискеroomId, как и ожидалось.
Это работает и для функций:
Вы можете писать собственные функции для группировки частей логики внутри вашего эффекта. Пока вы также объявляете ихвнутривашего эффекта, они не являются реактивными значениями и поэтому не должны быть зависимостями вашего эффекта.
Чтение примитивных значений из объектов
Иногда вы можете получать объект из пропсов:
Риск здесь в том, что родительский компонент будет создавать объект во время рендеринга:
Это приведёт к переподключению вашего эффекта каждый раз, когда родительский компонент перерендеривается. Чтобы это исправить, читайте информацию из объектавнеэффекта и избегайте зависимостей от объектов и функций:
Логика становится немного повторяющейся (вы читаете некоторые значения из объекта вне эффекта, а затем создаёте объект с теми же значениями внутри эффекта). Но это делает очень явным, от какой информации ваш эффектфактическизависит. Если объект будет случайно пересоздан родительским компонентом, чат не переподключится. Однако, еслиoptions.roomIdилиoptions.serverUrlдействительно отличаются, чат переподключится.
Вычисление примитивных значений из функций
Тот же подход может работать и для функций. Например, предположим, что родительский компонент передаёт функцию:
Чтобы избежать её добавления в зависимости (и, как следствие, переподключения при перерендерах), вызовите её вне эффекта. Это даст вам значенияroomId и serverUrl, которые не являются объектами и которые вы можете прочитать внутри вашего эффекта:
Это работает только длячистыхфункций, потому что их безопасно вызывать во время рендеринга. Если ваша функция является обработчиком событий, но вы не хотите, чтобы её изменения вызывали повторную синхронизацию вашего эффекта,оберните её в событие эффекта.
Итоги
- Зависимости всегда должны соответствовать коду.
- Если вас не устраивают ваши зависимости, вам нужно редактировать код.
- Игнорирование линтера приводит к очень запутанным ошибкам, и вам всегда следует этого избегать.
- Чтобы удалить зависимость, вам нужно «доказать» линтеру, что она не нужна.
- Если какой-то код должен выполняться в ответ на конкретное взаимодействие, переместите этот код в обработчик события.
- Если разные части вашего эффекта должны перезапускаться по разным причинам, разделите его на несколько эффектов.
- Если вы хотите обновить состояние на основе предыдущего состояния, передайте функцию-обновитель.
- Если вы хотите прочитать последнее значение без «реакции» на него, извлеките из вашего эффекта событие эффекта.
- В JavaScript объекты и функции считаются разными, если они были созданы в разное время.
- Старайтесь избегать зависимостей от объектов и функций. Выносите их за пределы компонента или внутрь эффекта.
Try out some challenges
Challenge 1 of 4:Fix a resetting interval #
This Effect sets up an interval that ticks every second. You’ve noticed something strange happening: it seems like the interval gets destroyed and re-created every time it ticks. Fix the code so that the interval doesn’t get constantly re-created.
