v19.2Latest

Повторное использование логики с помощью пользовательских хуков

React включает несколько встроенных хуков, таких какuseState,useContext и useEffect. Иногда вам может понадобиться хук для более специфической цели: например, для получения данных, отслеживания состояния подключения пользователя к сети или подключения к чату. Возможно, вы не найдете такие хуки в React, но вы можете создать собственные хуки для нужд вашего приложения.

Вы узнаете
  • Что такое пользовательские хуки и как их писать
  • Как повторно использовать логику между компонентами
  • Как называть и структурировать ваши пользовательские хуки
  • Когда и зачем извлекать пользовательские хуки

Пользовательские хуки: общая логика между компонентами

Представьте, что вы разрабатываете приложение, которое сильно зависит от сети (как и большинство приложений). Вы хотите предупредить пользователя, если его сетевое соединение случайно прервалось во время использования вашего приложения. Как бы вы это реализовали? Похоже, что вашему компоненту понадобятся две вещи:

  1. Состояние, которое отслеживает, находится ли сеть в режиме онлайн.
  2. Эффект, который подписывается на глобальные событияonline и offlineи обновляет это состояние.

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

Попробуйте включить и выключить сеть и обратите внимание, как этотStatusBarреагирует на ваши действия.

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

Для начала вы можете скопировать и вставить состояниеisOnlineи Эффект в компонентSaveButton:

Убедитесь, что при отключении сети кнопка изменит свой внешний вид.

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

Извлечение собственного пользовательского хука из компонента

Представьте на мгновение, что, подобноuseState и useEffect, существовал встроенный хукuseOnlineStatus. Тогда оба этих компонента можно было бы упростить, и вы могли бы убрать дублирование между ними:

Хотя такого встроенного хука нет, вы можете написать его сами. Объявите функцию с именемuseOnlineStatusи переместите в неё весь дублирующийся код из ранее написанных компонентов:

В конце функции вернитеisOnline. Это позволяет вашим компонентам читать это значение:

Убедитесь, что переключение сети обновляет оба компонента.

Теперь в ваших компонентах не так много повторяющейся логики.Что важнее, код внутри них описываетчто они хотят сделать(использовать статус онлайн!), а некак это сделать(путем подписки на события браузера).

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

Имена хуков всегда начинаются сuse

React-приложения строятся из компонентов. Компоненты строятся из хуков, будь то встроенные или пользовательские. Вы, вероятно, часто будете использовать пользовательские хуки, созданные другими, но иногда можете написать свой собственный!

Вы должны соблюдать следующие соглашения об именовании:

  1. Имена React-компонентов должны начинаться с заглавной буквы,напримерStatusBar и SaveButton. React-компоненты также должны возвращать то, что React умеет отображать, например, кусок JSX.
  2. Имена хуков должны начинаться сuse, за которым следует заглавная буква,напримерuseState(встроенный) илиuseOnlineStatus(пользовательский, как ранее на странице). Хуки могут возвращать произвольные значения.

Это соглашение гарантирует, что вы всегда можете посмотреть на компонент и понять, где могут «скрываться» его состояние, эффекты и другие возможности React. Например, если вы видите вызов функцииgetColor()внутри вашего компонента, вы можете быть уверены, что она не может содержать состояние React, потому что её имя не начинается сuse. Однако вызов функции, такой какuseOnlineStatus(), скорее всего, будет содержать вызовы других хуков внутри!

Примечание

Если ваш линтернастроен для React,он будет применять это соглашение об именовании. Прокрутите вверх к песочнице выше и переименуйтеuseOnlineStatus в getOnlineStatus. Обратите внимание, что линтер не позволит вам вызыватьuseStateилиuseEffectвнутри неё. Только хуки и компоненты могут вызывать другие хуки!

Deep Dive
Должны ли все функции, вызываемые во время рендеринга, начинаться с префикса use?

Пользовательские хуки позволяют делиться логикой состояния, а не самим состоянием

В предыдущем примере, когда вы включали и выключали сеть, оба компонента обновлялись вместе. Однако ошибочно думать, что одна переменная состоянияisOnlineявляется общей между ними. Посмотрите на этот код:

Это работает так же, как и до извлечения дублирования:

Это две полностью независимые переменные состояния и Эффекты! Они оказались с одинаковым значением в один и тот же момент времени, потому что вы синхронизировали их с одним и тем же внешним значением (включена ли сеть).

Чтобы лучше это проиллюстрировать, нам понадобится другой пример. Рассмотрим этот компонентForm:

Здесь есть повторяющаяся логика для каждого поля формы:

  1. Есть часть состояния (firstName и lastName).
  2. Есть обработчик изменений (handleFirstNameChange и handleLastNameChange).
  3. Есть часть JSX, которая задаёт атрибутыvalue и onChangeдля этого поля ввода.

Вы можете вынести повторяющуюся логику в этот пользовательский ХукuseFormInput:

Обратите внимание, что он объявляеттолько однупеременную состояния с именемvalue.

Однако компонентFormвызываетuseFormInputдважды:

Вот почему это работает как объявление двух отдельных переменных состояния!

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

Когда вам нужно поделиться самим состоянием между несколькими компонентами,поднимите его вверх и передайте вниз.

Передача реактивных значений между хуками

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

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

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

Теперь переместите код Эффекта в пользовательский Хук:

Это позволяет вашему компонентуChatRoomвызывать ваш пользовательский Хук, не беспокоясь о том, как он работает внутри:

Это выглядит намного проще! (Но делает то же самое.)

Обратите внимание, что логикапо-прежнему реагируетна изменения пропсов и состояния. Попробуйте изменить URL сервера или выбранную комнату:

Обратите внимание, как вы берёте возвращаемое значение одного хука:

и передаёте его как входные данные другому хуку:

Каждый раз, когда ваш компонентChatRoomперерисовывается, он передаёт последние значенияroomId и serverUrlв ваш хук. Вот почему ваш эффект переподключается к чату всякий раз, когда их значения изменяются после перерисовки. (Если вы когда-либо работали с программным обеспечением для обработки аудио или видео, такое последовательное соединение хуков может напомнить вам о цепочках визуальных или аудиоэффектов. Это как если бы выводuseState«подаётся» на вход хукаuseChatRoom.)

Передача обработчиков событий в пользовательские хуки

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

Допустим, вы хотите вернуть эту логику обратно в ваш компонент:

Чтобы это заработало, измените ваш пользовательский хук так, чтобы он принималonReceiveMessageв качестве одного из именованных параметров:

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

Добавление зависимости отonReceiveMessageне идеально, потому что это приведёт к повторному подключению чата при каждом повторном рендеринге компонента.Оберните этот обработчик событий в событие эффекта, чтобы удалить его из зависимостей:

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

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

Когда использовать пользовательские хуки

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

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

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

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

Теперь вы можете заменить оба Эффекта в компонентеShippingFormвызовамиuseData:

Извлечение пользовательского хука делает поток данных явным. Вы передаетеurlи получаетеdataна выходе. «Скрывая» ваш Эффект внутриuseData, вы также предотвращаете добавлениеShippingForm. Со временем большинство Эффектов в вашем приложении будут находиться в пользовательских хуках.ненужных зависимостейк нему тем, кто работает над компонентом

Deep Dive
Сосредоточьте свои пользовательские хуки на конкретных высокоуровневых сценариях использования

Пользовательские хуки помогают перейти к лучшим паттернам

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

Вернёмся к этому примеру:

В приведённом выше примереuseOnlineStatusреализован с помощью парыuseState и useEffect.Однако это не самое лучшее возможное решение. Есть ряд крайних случаев, которые оно не учитывает. Например, оно предполагает, что при монтировании компонентаisOnline уже true, но это может быть неверно, если сеть уже отключилась. Вы можете использовать браузерный APInavigator.onLine, чтобы проверить это, но его прямое использование не будет работать на сервере для генерации начального HTML. Короче говоря, этот код можно улучшить.

React включает специальный API под названиемuseSyncExternalStore, который решает все эти проблемы за вас. Вот ваш хукuseOnlineStatus, переписанный с использованием этого нового API:

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

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

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

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

Deep Dive
Предоставит ли React какое-либо встроенное решение для получения данных?

Существует более одного способа сделать это

Допустим, вы хотите реализовать анимацию плавного появленияс нуля, используя браузерный APIrequestAnimationFrame. Вы можете начать с эффекта, который настраивает цикл анимации. В каждом кадре анимации вы можете изменять непрозрачность DOM-узла, который выхраните в ref, пока она не достигнет1. Ваш код может начинаться так:

Чтобы сделать компонент более читаемым, вы можете вынести логику в пользовательский хукuseFadeIn:

Вы можете оставить кодuseFadeInкак есть, но также можете рефакторить его дальше. Например, вы можете вынести логику настройки цикла анимации изuseFadeInв пользовательский хукuseAnimationLoop:

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

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

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

Иногда вам даже не нужен Хук!

Резюме

  • Пользовательские Хуки позволяют вам делиться логикой между компонентами.
  • Пользовательские Хуки должны называться, начиная сuse, за которым следует заглавная буква.
  • Пользовательские Хуки делятся только логикой состояния, а не самим состоянием.
  • Вы можете передавать реактивные значения из одного Хука в другой, и они остаются актуальными.
  • Все Хуки перезапускаются каждый раз при повторном рендере вашего компонента.
  • Код ваших пользовательских Хуков должен быть чистым, как и код вашего компонента.
  • Оберните обработчики событий, полученные пользовательскими Хуками, в События Эффектов.
  • Не создавайте пользовательские Хуки типаuseMount. Сохраняйте их назначение конкретным.
  • Вам решать, как и где выбирать границы вашего кода.

Try out some challenges

Challenge 1 of 5:Extract a useCounter Hook #

This component uses a state variable and an Effect to display a number that increments every second. Extract this logic into a custom Hook called useCounter. Your goal is to make the Counter component implementation look exactly like this:

export default function Counter() {
  const count = useCounter();
  return <h1>Seconds passed: {count}</h1>;
}

You’ll need to write your custom Hook in useCounter.js and import it into the App.js file.