v19.2Latest

Возможно, вам не нужен эффект

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

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

Как удалить ненужные эффекты

Существует два распространённых случая, когда эффекты не нужны:

  • Эффекты не нужны для преобразования данных для рендеринга.Например, предположим, вы хотите отфильтровать список перед его отображением. У вас может возникнуть соблазн написать эффект, который обновляет переменную состояния при изменении списка. Однако это неэффективно. Когда вы обновляете состояние, React сначала вызовет ваши функции компонентов, чтобы вычислить, что должно быть на экране. Затем React«зафиксирует»эти изменения в DOM, обновив экран. Затем React запустит ваши эффекты. Если ваш эффекттакженемедленно обновляет состояние, весь процесс начинается заново! Чтобы избежать лишних проходов рендеринга, преобразуйте все данные на верхнем уровне ваших компонентов. Этот код будет автоматически перезапускаться при каждом изменении ваших пропсов или состояния.
  • Эффекты не нужны для обработки пользовательских событий.Например, предположим, вы хотите отправить POST-запрос на/api/buyи показать уведомление, когда пользователь покупает продукт. В обработчике события клика по кнопке «Купить» вы точно знаете, что произошло. К моменту запуска эффекта вы не знаете,чтосделал пользователь (например, по какой кнопке кликнули). Вот почему обработку пользовательских событий обычно выполняют в соответствующих обработчиках событий.

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

Чтобы помочь вам развить правильную интуицию, давайте рассмотрим несколько распространённых конкретных примеров!

Обновление состояния на основе пропсов или состояния

Предположим, у вас есть компонент с двумя переменными состояния:firstName и lastName. Вы хотите вычислитьfullNameиз них, объединив их. Более того, вы хотите, чтобыfullNameобновлялся при каждом измененииfirstNameилиlastName. Ваша первая мысль может заключаться в добавлении переменной состоянияfullNameи её обновлении в эффекте:

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

Если что-то можно вычислить из существующих пропсов или состояния,не помещайте это в состояние.Вместо этого вычисляйте это во время рендеринга.Это делает ваш код быстрее (вы избегаете лишних «каскадных» обновлений), проще (вы удаляете часть кода) и менее подверженным ошибкам (вы избегаете ошибок, вызванных рассинхронизацией различных переменных состояния). Если этот подход кажется вам новым,Мышление в Reactобъясняет, что должно попадать в состояние.

Кэширование дорогих вычислений

Этот компонент вычисляетvisibleTodos, беряtodos, полученные через пропсы, и фильтруя их в соответствии с пропсомfilter. У вас может возникнуть соблазн сохранить результат в состоянии и обновлять его из эффекта:

Как и в предыдущем примере, это и не нужно, и неэффективно. Сначала удалите состояние и эффект:

Обычно этот код в порядке! Но, возможно,getFilteredTodos()работает медленно или у вас очень многоtodos. В таком случае вы не захотите пересчитыватьgetFilteredTodos(), если изменилась какая-то несвязанная переменная состояния, напримерnewTodo.

Вы можете кэшировать (или«мемоизировать») дорогостоящее вычисление, обернув его в хукuseMemo:

Примечание

React Compilerможет автоматически мемоизировать дорогостоящие вычисления за вас, устраняя необходимость в ручном использованииuseMemoво многих случаях.

Или, записанное в одну строку:

Это говорит React, что вы не хотите, чтобы внутренняя функция выполнялась повторно, если только не изменилисьtodosилиfilter.React запомнит возвращаемое значениеgetFilteredTodos()во время первоначального рендера. Во время следующих рендеров он проверит, отличаются лиtodosилиfilter. Если они такие же, как в прошлый раз,useMemoвернёт последний сохранённый результат. Но если они отличаются, React снова вызовет внутреннюю функцию (и сохранит её результат).

Функция, которую вы оборачиваете вuseMemo, выполняется во время рендеринга, поэтому это работает только длячистых вычислений.

Deep Dive
Как определить, что вычисление затратное?

Сброс всего состояния при изменении пропса

КомпонентProfilePageполучает пропсuserId. Страница содержит поле ввода комментария, и вы используете переменную состоянияcommentдля хранения его значения. Однажды вы замечаете проблему: при переходе от одного профиля к другому состояниеcommentне сбрасывается. В результате легко случайно опубликовать комментарий в профиле не того пользователя. Чтобы исправить проблему, вы хотите очистить переменную состоянияcommentвсякий раз, когда изменяетсяuserId:

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

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

Обычно React сохраняет состояние, когда один и тот же компонент отрисовывается в том же месте.ПередаваяuserIdв качествеkeyкомпонентуProfile, вы просите React рассматривать два компонентаProfileс разнымиuserIdкак два разных компонента, которые не должны делиться никаким состоянием.Всякий раз, когда ключ (который вы установили вuserId) изменяется, React пересоздаст DOM исбросит состояниекомпонентаProfileи всех его дочерних компонентов. Теперь полеcommentбудет автоматически очищаться при переходе между профилями.

Обратите внимание, что в этом примере только внешний компонентProfilePageэкспортируется и виден другим файлам в проекте. Компоненты, отрисовывающиеProfilePage, не должны передавать ему ключ: они передаютuserIdкак обычный пропс. То, чтоProfilePageпередаёт его какkeyвнутреннему компонентуProfile, является деталью реализации.

Изменение части состояния при изменении пропса

Иногда может потребоваться сбросить или изменить часть состояния при изменении пропса, но не всё состояние целиком.

Этот компонентListполучает списокitemsв качестве пропса и хранит выбранный элемент в переменной состоянияselection. Вы хотите сброситьselection в nullвсякий раз, когда пропсitemsполучает другой массив:

Это тоже не идеально. Каждый раз, когдаitemsменяются,Listи его дочерние компоненты сначала отрисуются с устаревшим значениемselection. Затем React обновит DOM и запустит Эффекты. Наконец, вызовsetSelection(null)вызовет ещё одну перерисовкуListи его дочерних компонентов, запуская весь этот процесс заново.

Начните с удаления Эффекта. Вместо этого изменяйте состояние непосредственно во время рендеринга:

Хранение информации из предыдущих рендеровтаким образом может быть сложно для понимания, но это лучше, чем обновлять то же состояние в Эффекте. В приведённом выше примереsetSelectionвызывается непосредственно во время рендеринга. React перерисуетListнемедленнопосле его завершения с операторомreturn. React ещё не отрисовал дочерние элементыListи не обновил DOM, поэтому это позволяет дочерним элементамListпропустить рендеринг устаревшего значенияselection.

Когда вы обновляете компонент во время рендеринга, React отбрасывает возвращённый JSX и немедленно повторяет рендеринг. Чтобы избежать очень медленных каскадных повторных попыток, React позволяет обновлять состояние толькотого же самогокомпонента во время рендеринга. Если вы обновите состояние другого компонента во время рендеринга, вы увидите ошибку. Условие типаitems !== prevItemsнеобходимо, чтобы избежать циклов. Вы можете изменять состояние таким образом, но любые другие побочные эффекты (например, изменение DOM или установка таймаутов) должны оставаться в обработчиках событий или Эффектах, чтобысохранять компоненты чистыми.

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

Теперь вообще не нужно «настраивать» состояние. Если элемент с выбранным ID находится в списке, он остаётся выбранным. Если его нет, тоselection, вычисленный во время рендеринга, будет равенnull, потому что соответствующий элемент не был найден. Это поведение отличается, но, возможно, лучше, поскольку большинство измененийitemsсохраняют выбор.

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

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

Этот эффект не нужен. Он также, скорее всего, вызовет ошибки. Например, предположим, что ваше приложение «запоминает» корзину между перезагрузками страницы. Если вы добавите товар в корзину один раз и обновите страницу, уведомление появится снова. Оно будет появляться каждый раз при обновлении страницы этого товара. Это происходит потому, чтоproduct.isInCartуже будетtrueпри загрузке страницы, поэтому указанный выше эффект вызоветshowNotification().

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

Это и удаляет ненужный эффект, и исправляет ошибку.

Отправка POST-запроса

Этот компонентFormотправляет два вида POST-запросов. Он отправляет аналитическое событие при монтировании. Когда вы заполняете форму и нажимаете кнопку Отправить, он отправит POST-запрос на конечную точку/api/register:

Применим те же критерии, что и в предыдущем примере.

POST-запрос для аналитики должен оставаться в эффекте. Это связано с тем, чтопричинаотправки аналитического события — отображение формы. (В разработке он сработает дважды, носм. здесь, как с этим справиться.)

Однако POST-запрос/api/register не вызван отображениемформы. Вы хотите отправить запрос только в один конкретный момент: когда пользователь нажимает кнопку. Это должно происходить толькопри этом конкретном взаимодействии. Удалите второй эффект и переместите этот POST-запрос в обработчик события:

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

Цепочки вычислений

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

В этом коде есть две проблемы.

Первая проблема заключается в том, что он очень неэффективен: компонент (и его дочерние элементы) должны перерисовываться между каждым вызовомsetв цепочке. В приведённом выше примере, в худшем случае (setCard→ рендер →setGoldCardCount→ рендер →setRound→ рендер →setIsGameOver→ рендер) происходит три лишних перерисовки дерева ниже.

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

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

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

Помните, что внутри обработчиков событийсостояние ведёт себя как снимок.Например, даже после вызоваsetRound(round + 1)переменнаяroundбудет отражать значение на момент нажатия пользователем кнопки. Если вам нужно использовать следующее значение для вычислений, определите его вручную, например, какconst nextRound = round + 1.

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

Инициализация приложения

Некоторую логику следует выполнять только один раз при загрузке приложения.

Возможно, у вас возникнет соблазн поместить её в эффект в компоненте верхнего уровня:

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

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

Вы также можете выполнить её во время инициализации модуля и до рендеринга приложения:

Код на верхнем уровне выполняется один раз при импорте вашего компонента — даже если он в итоге не будет отрисован. Чтобы избежать замедления или неожиданного поведения при импорте произвольных компонентов, не злоупотребляйте этим паттерном. Держите логику инициализации всего приложения в корневых модулях компонентов, таких какApp.js, или в точке входа вашего приложения.

Уведомление родительских компонентов об изменениях состояния

Допустим, вы пишете компонентToggleс внутренним состояниемisOn, которое может быть либоtrue, либоfalse. Есть несколько различных способов его переключения (щелчком или перетаскиванием). Вы хотите уведомлять родительский компонент всякий раз, когда внутреннее состояниеToggleизменяется, поэтому вы предоставляете событиеonChangeи вызываете его из эффекта:

Как и ранее, это не идеально. КомпонентToggleсначала обновляет своё состояние, и React обновляет экран. Затем React запускает эффект, который вызывает функциюonChange, переданную из родительского компонента. Теперь родительский компонент обновит своё собственное состояние, запустив ещё один цикл рендеринга. Лучше выполнить всё за один проход.

Удалите эффект и вместо этого обновите состояниеобоихкомпонентов в одном обработчике события:

При таком подходе и компонентToggle, и его родительский компонент обновляют своё состояние во время события. Reactгруппирует обновленияиз разных компонентов вместе, поэтому будет только один цикл рендеринга.

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

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

Передача данных родителю

Этот компонентChildполучает некоторые данные, а затем передаёт их компонентуParentв эффекте:

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

Это проще и делает поток данных предсказуемым: данные передаются сверху вниз — от родителя к дочернему компоненту.

Подписка на внешнее хранилище

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

Здесь компонент подписывается на внешнее хранилище данных (в данном случае — браузерный APInavigator.onLine). Поскольку этот API не существует на сервере (поэтому его нельзя использовать для начального HTML), изначально состояние устанавливается вtrue. Всякий раз, когда значение этого хранилища данных изменяется в браузере, компонент обновляет своё состояние.

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

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

Получение данных

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

Вамне нужнопереносить этот запрос в обработчик события.

Это может показаться противоречием с предыдущими примерами, где вам нужно было помещать логику в обработчики событий! Однако учтите, что основная причина для выполнения запроса — это несобытие ввода текста. Поля поиска часто предзаполняются из URL, и пользователь может перемещаться по истории Назад и Вперёд, не касаясь поля ввода.

Неважно, откуда берутсяpage и query. Пока этот компонент виден, вы хотите поддерживатьresultsсинхронизированнымис данными из сети для текущихpage и query. Вот почему это Эффект.

Однако в приведённом выше коде есть ошибка. Представьте, что вы быстро вводите"hello". Тогдаqueryбудет меняться от"h" к "he","hel","hell" и "hello". Это запустит отдельные запросы, но нет гарантии, в каком порядке придут ответы. Например, ответ на"hell"может прийтипослеответа на"hello". Поскольку последним будет вызванsetResults(), вы будете отображать неправильные результаты поиска. Это называется«состоянием гонки»: два разных запроса «соревновались» друг с другом и пришли в порядке, отличном от ожидаемого.

Чтобы исправить состояние гонки, необходимодобавить функцию очистки, чтобы игнорировать устаревшие ответы:

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

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

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

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

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

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

Резюме

  • Если что-то можно вычислить во время рендеринга, эффект не нужен.
  • Для кэширования дорогих вычислений добавьтеuseMemoвместоuseEffect.
  • Чтобы сбросить состояние всего дерева компонентов, передайте ему другойkey.
  • Чтобы сбросить определённую часть состояния в ответ на изменение пропса, задайте его во время рендеринга.
  • Код, который выполняется, потому что компонент былотображён, должен быть в эффектах, остальное — в событиях.
  • Если нужно обновить состояние нескольких компонентов, лучше сделать это в рамках одного события.
  • Когда вы пытаетесь синхронизировать переменные состояния в разных компонентах, рассмотрите поднятие состояния.
  • Вы можете получать данные с помощью эффектов, но нужно реализовать очистку, чтобы избежать состояний гонки.

Try out some challenges

Challenge 1 of 4:Transform data without Effects #

The TodoList below displays a list of todos. When the “Show only active todos” checkbox is ticked, completed todos are not displayed in the list. Regardless of which todos are visible, the footer displays the count of todos that are not yet completed.

Simplify this component by removing all the unnecessary state and Effects.