Переиспользование логики с помощью пользовательских хуков¶
React поставляется с несколькими встроенными хуками, такими как useState
, useContext
и useEffect
. Иногда вам захочется иметь хук для какой-то более конкретной цели: например, для получения данных, отслеживания того, находится ли пользователь в сети, или для подключения к чату. Возможно, вы не найдете таких хуков в React, но вы можете создать свои собственные хуки для нужд вашего приложения.
Вы узнаете
- Что такое пользовательские хуки и как написать свой собственный
- Как повторно использовать логику между компонентами
- Как назвать и структурировать пользовательские хуки
- Когда и зачем извлекать пользовательские хуки
Пользовательские хуки: Совместное использование логики между компонентами¶
Представьте, что вы разрабатываете приложение, которое сильно зависит от сети (как и большинство приложений). Вы хотите предупредить пользователя, если его сетевое соединение случайно прервалось во время работы с вашим приложением. Как вы собираетесь это сделать? Похоже, что вам понадобятся две вещи в вашем компоненте:
- Элемент состояния, который отслеживает, находится ли сеть в сети.
- Эффект, который подписывается на глобальные события
online
иoffline
и обновляет это состояние.
Это позволит вашему компоненту синхронизироваться со статусом сети. Вы можете начать с чего-то подобного:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
Попробуйте включить и выключить сеть, и обратите внимание, как эта StatusBar
обновляется в ответ на ваши действия.
Теперь представьте, что вы также хотите использовать ту же логику в другом компоненте. Вы хотите реализовать кнопку Save, которая будет отключена и показывать "Reconnecting..." вместо "Save", пока сеть выключена.
Для начала вы можете скопировать и вставить состояние isOnline
и эффект в SaveButton
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
Убедитесь, что при отключении сети кнопка изменит свой вид.
Эти два компонента работают нормально, но дублирование логики между ними вызывает сожаление. Похоже, что даже если они имеют разный визуальный вид, вы хотите повторно использовать логику между ними.
Извлечение собственного пользовательского хука из компонента¶
Представьте на секунду, что, подобно useState
и useEffect
, существует встроенный хук useOnlineStatus
. Тогда оба этих компонента можно было бы упростить и убрать дублирование между ними:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Хотя такого встроенного Hook не существует, вы можете написать его самостоятельно. Объявите функцию useOnlineStatus
и перенесите в нее весь дублирующийся код из компонентов, которые вы написали ранее:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
В конце функции верните isOnline
. Это позволит вашим компонентам прочитать это значение:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Убедитесь, что включение и выключение сети обновляет оба компонента.
Теперь в ваших компонентах не так много повторяющейся логики. Более того, код внутри них описывает что они хотят сделать (использовать сетевой статус!), а не как это сделать (подписываясь на события браузера).
Когда вы извлекаете логику в пользовательские Hooks, вы можете скрыть ужасные детали того, как вы работаете с какой-то внешней системой или API браузера. Код ваших компонентов выражает ваше намерение, а не реализацию.
Имена хуков всегда начинаются с use
¶
Приложения React строятся из компонентов. Компоненты строятся из хуков, встроенных или пользовательских. Скорее всего, вы часто будете использовать пользовательские хуки, созданные другими, но иногда вы можете написать один самостоятельно!
Вы должны следовать этим соглашениям об именовании:
- Имена компонентов React должны начинаться с заглавной буквы, например,
StatusBar
иSaveButton
. Компоненты React также должны возвращать что-то, что React умеет отображать, например, кусок JSX. - Имена хуков должны начинаться с
use
, за которым следует заглавная буква, напримерuseState
(встроенный) илиuseOnlineStatus
(пользовательский, как ранее на этой странице). Хуки могут возвращать произвольные значения.
Это соглашение гарантирует, что вы всегда сможете посмотреть на компонент и узнать, где может "прятаться" его состояние, Эффекты и другие возможности React. Например, если вы видите вызов функции getColor()
внутри вашего компонента, вы можете быть уверены, что она не может содержать внутри себя состояние React, потому что ее имя не начинается с use
. Однако вызов такой функции, как useOnlineStatus()
, скорее всего, будет содержать вызовы других Hooks внутри!
Если ваш линтер настроен на React, он будет применять это соглашение об именовании. Прокрутите вверх до песочницы выше и переименуйте useOnlineStatus
в getOnlineStatus
. Обратите внимание, что линтер больше не позволит вам вызывать useState
или useEffect
внутри него. Только Hooks и компоненты могут вызывать другие Hooks!
Должны ли все функции, вызываемые во время рендеринга, начинаться с префикса use?
Нет. Функции, которые не вызывают Hooks, не обязаны быть Hooks.
Если ваша функция не вызывает никаких хуков, избегайте префикса use
. Вместо этого напишите ее как обычную функцию без префикса use
. Например, useSorted
ниже не вызывает хуков, поэтому вместо этого назовите ее getSorted
:
1 2 3 4 5 6 7 8 9 |
|
Это гарантирует, что ваш код сможет вызвать эту регулярную функцию в любом месте, включая условия:
1 2 3 4 5 6 7 8 |
|
Вы должны дать префикс use
функции (и таким образом сделать ее хуком), если она использует хотя бы один хук внутри себя:
1 2 3 4 |
|
Технически, React этого не делает. В принципе, вы можете сделать хук, который не вызывает другие хуки. Это часто запутывает и ограничивает, поэтому лучше избегать такого шаблона. Однако в редких случаях это может быть полезно. Например, возможно, ваша функция сейчас не использует никаких хуков, но в будущем вы планируете добавить в нее несколько вызовов хуков. Тогда имеет смысл назвать ее с префиксом use
:
1 2 3 4 5 6 |
|
Тогда компоненты не смогут вызывать его условно. Это станет важным, когда вы действительно добавите вызовы Hook внутри. Если вы не планируете использовать в нем хуки (сейчас или позже), не делайте его хуком.
Пользовательские хуки позволяют вам делиться логикой состояния, а не самим состоянием¶
В предыдущем примере, когда вы включали и выключали сеть, оба компонента обновлялись вместе. Однако неправильно думать, что одна переменная состояния isOnline
разделяется между ними. Посмотрите на этот код:
1 2 3 4 5 6 7 8 9 |
|
Он работает так же, как и до извлечения дубликата:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Это две совершенно независимые переменные состояния и Effects! Они имеют одинаковое значение в одно и то же время, потому что вы синхронизировали их с одним и тем же внешним значением (включена ли сеть).
Чтобы лучше проиллюстрировать это, нам понадобится другой пример. Рассмотрим компонент Form
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
Есть несколько повторяющихся логических операций для каждого поля формы:
- Есть часть состояния (
firstName
иlastName
). - Есть обработчик изменений (
handleFirstNameChange
иhandleLastNameChange
). - Есть кусок JSX, который определяет атрибуты
value
иonChange
для этого входа.
Вы можете извлечь повторяющуюся логику в этот пользовательский хук useFormInput
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Обратите внимание, что здесь объявлена только одна переменная состояния под названием value
.
Однако, компонент Form
вызывает useFormInput
два раза:
1 2 3 4 5 |
|
Вот почему это работает как объявление двух отдельных переменных состояния!
Настроенные хуки позволяют вам делиться логикой состояния, но не самим состоянием. Каждый вызов хука полностью независим от любого другого вызова того же хука. Вот почему две вышеприведенные песочницы полностью эквивалентны. Если хотите, прокрутите страницу назад и сравните их. Поведение до и после извлечения пользовательского хука идентично.
Если вам нужно разделить само состояние между несколькими компонентами, вместо этого lift it up and pass it down.
Передача реактивных значений между хуками¶
Код внутри ваших пользовательских хуков будет выполняться заново при каждом повторном рендеринге компонента. Вот почему, как и компоненты, пользовательские хуки должны быть чистыми. Думайте о коде пользовательских хуков как о части тела вашего компонента!
Поскольку пользовательские хуки перерисовываются вместе с вашим компонентом, они всегда получают последние пропсы и состояние. Чтобы понять, что это значит, рассмотрим пример с чатом. Измените URL сервера или чата:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Когда вы изменяете serverUrl
или roomId
, Эффект "реагирует" на ваши изменения и пересинхронизируется. По сообщениям консоли вы можете определить, что чат переподключается каждый раз, когда вы изменяете зависимости вашего Эффекта.
Теперь переместите код эффекта в пользовательский хук:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Это позволит вашему компоненту ChatRoom
вызывать ваш пользовательский Hook, не беспокоясь о том, как он работает внутри:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Это выглядит намного проще! Но делает то же самое.
Обратите внимание, что логика все еще реагирует на изменения пропса и состояния. Попробуйте отредактировать URL сервера или выбранной комнаты:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Обратите внимание, что вы берете возвращаемое значение одного Hook:
1 2 3 4 5 6 7 8 9 10 11 |
|
и передать его в качестве входного сигнала в другой Hook:
1 2 3 4 5 6 7 8 9 10 11 |
|
Каждый раз, когда ваш компонент ChatRoom
перерисовывается, он передает последние значения roomId
и serverUrl
вашему Hook. Именно поэтому ваш Эффект повторно подключается к чату, когда их значения меняются после повторного рендеринга. (Если вы когда-нибудь работали с программами для обработки аудио или видео, то такое построение цепочки хуков может напомнить вам построение цепочки визуальных или звуковых эффектов. Это как если бы выход useState
"вливался" во вход useChatRoom
).
Передача обработчиков событий пользовательским хукам¶
В разработке
Этот раздел описывает экспериментальный API, который еще не был выпущен в стабильной версии React.
Когда вы начнете использовать useChatRoom
в большем количестве компонентов, вы, возможно, захотите позволить компонентам настраивать его поведение. Например, в настоящее время логика того, что делать, когда приходит сообщение, жестко закодирована внутри Hook:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Допустим, вы хотите перенести эту логику обратно в ваш компонент:
1 2 3 4 5 6 7 8 9 10 11 |
|
Чтобы это работало, измените свой пользовательский хук так, чтобы он принимал onReceiveMessage
в качестве одной из именованных опций:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Это будет работать, но есть еще одно улучшение, которое вы можете сделать, когда ваш пользовательский Hook принимает обработчики событий.
Добавление зависимости от onReceiveMessage
не является идеальным, потому что это заставит чат переподключаться каждый раз, когда компонент перерендерится. Заверните этот обработчик события в событие эффекта, чтобы избавить его от зависимостей:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Теперь чат не будет подключаться заново каждый раз, когда компонент ChatRoom
перерисовывается. Вот полностью рабочий демонстрационный пример передачи обработчика события пользовательскому Hook, с которым вы можете поиграть:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Обратите внимание, что вам больше не нужно знать, как работает useChatRoom
, чтобы использовать его. Вы можете добавить его в любой другой компонент, передать любые другие параметры, и он будет работать точно так же. В этом и заключается сила пользовательских хуков.
Когда использовать пользовательские хуки¶
Вам не нужно извлекать пользовательский хук для каждого маленького дублирующегося кусочка кода. Некоторое дублирование вполне нормально. Например, извлечение хука useFormInput
для обертывания одного вызова useState
, как это было ранее, вероятно, не нужно.
Однако всякий раз, когда вы пишете Эффект, подумайте, не будет ли яснее, если его также обернуть в пользовательский Хук. Эффекты не должны требоваться очень часто, поэтому если вы пишете эффект, это означает, что вам нужно "выйти за пределы React", чтобы синхронизироваться с какой-то внешней системой или сделать что-то, для чего у React нет встроенного API. Обернув это в пользовательский хук, вы можете точно передать свое намерение и то, как данные проходят через него.
Например, рассмотрим компонент ShippingForm
, который отображает два выпадающих списка: один показывает список городов, а другой - список областей в выбранном городе. Вы можете начать с кода, который выглядит следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
Хотя этот код довольно повторяющийся, правильно держать эти Эффекты отдельно друг от друга. Они синхронизируют две разные вещи, поэтому не стоит объединять их в один Эффект. Вместо этого вы можете упростить компонент ShippingForm
выше, извлекая общую логику между ними в свой собственный хук useData
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Теперь вы можете заменить оба Effects в компоненте ShippingForm
вызовами useData
:
1 2 3 4 5 6 7 8 9 10 |
|
Извлечение пользовательского Hook делает поток данных явным. Вы вводите url
и получаете data
. "Пряча" свой Эффект внутри useData
, вы также не позволяете кому-то, работающему над компонентом ShippingForm
, добавить к нему ненужные зависимости. Со временем большая часть Эффектов вашего приложения будет находиться в пользовательских Hooks.
Сосредоточьте ваши пользовательские хуки на конкретных высокоуровневых сценариях использования
Начните с выбора имени вашего пользовательского хука. Если вы не можете выбрать четкое имя, это может означать, что ваш Эффект слишком связан с остальной логикой вашего компонента и еще не готов к извлечению.
В идеале название вашего пользовательского хука должно быть достаточно понятным, чтобы даже человек, который не часто пишет код, мог догадаться, что делает ваш пользовательский хук, что он принимает и что возвращает:
- ✅
useData(url)
- ✅
useImpressionLog(eventName, extraData)
- ✅
useChatRoom(options)
Когда вы синхронизируетесь с внешней системой, ваше пользовательское имя Hook может быть более техническим и использовать жаргон, характерный для этой системы. Это хорошо, если оно будет понятно человеку, знакомому с этой системой:
- ✅
useMediaQuery(query)
- ✅
useSocket(url)
- ✅
useIntersectionObserver(ref, options)
Избегайте создания и использования пользовательских хуков "жизненного цикла", которые действуют как альтернативы и удобные обертки для самого API useEffect
:
- 🔴
useMount(fn)
- 🔴
useEffectOnce(fn)
- 🔴
useUpdateEffect(fn)
Например, этот хук useMount
пытается обеспечить выполнение некоторого кода только "при монтировании":
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Пользовательские хуки "жизненного цикла", такие как useMount
, плохо вписываются в парадигму React. Например, в этом примере кода есть ошибка (он не "реагирует" на изменения roomId
или serverUrl
), но линтер не предупредит вас об этом, потому что линтер проверяет только прямые вызовы useEffect
. Он не будет знать о вашем хуке.
Если вы пишете эффект, начните с прямого использования React API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Затем вы можете (но не обязаны) извлекать пользовательские хуки для различных высокоуровневых сценариев использования:
1 2 3 4 5 6 7 8 9 10 |
|
Хороший пользовательский Hook делает вызывающий код более декларативным, ограничивая то, что он делает. Например, useChatRoom(options)
может только подключаться к чату, а useImpressionLog(eventName, extraData)
может только отправлять журнал впечатлений аналитику. Если ваш пользовательский API Hook не ограничивает сценарии использования и является очень абстрактным, в долгосрочной перспективе он, скорее всего, создаст больше проблем, чем решит.
Пользовательские хуки помогают перейти на лучшие паттерны¶
Эффекты - это "аварийный люк": вы используете их, когда вам нужно "выйти за пределы React" и когда нет лучшего встроенного решения для вашего случая использования. Со временем цель команды React - сократить количество Эффектов в вашем приложении до минимума, предоставляя более конкретные решения для более конкретных проблем. Обертывание ваших Эффектов в пользовательские Hooks упрощает обновление вашего кода, когда эти решения становятся доступными.
Давайте вернемся к этому примеру:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
В приведенном выше примере useOnlineStatus
реализован с помощью пары useState
и useEffect
. Однако это не лучшее из возможных решений. Оно не учитывает ряд побочных ситуаций. Например, предполагается, что когда компонент монтируется, isOnline
уже true
, но это может быть неверно, если сеть уже отключилась. Вы можете использовать API браузера navigator.onLine
для проверки этого, но его использование напрямую не будет работать на сервере для генерации начального HTML. Короче говоря, этот код можно улучшить.
К счастью, React 18 включает специальный API под названием useSyncExternalStore
, который решает все эти проблемы за вас. Вот как выглядит ваш хук useOnlineStatus
, переписанный для использования преимуществ этого нового API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Обратите внимание, что вам не нужно было менять ни один из компонентов, чтобы осуществить этот переход:
1 2 3 4 5 6 7 8 9 |
|
Это еще одна причина, по которой обертывание эффектов в пользовательские хуки часто оказывается полезным:
- Вы делаете поток данных к эффектам и от них очень явным.
- Вы позволяете компонентам сосредоточиться на замысле, а не на точной реализации ваших эффектов.
- Когда React добавляет новые возможности, вы можете удалить эти Эффекты, не меняя ни одного из своих компонентов.
По аналогии с системой проектирования вы можете найти полезным начать извлекать общие идиомы из компонентов вашего приложения в пользовательские Hooks. Это позволит сфокусировать код ваших компонентов на замысле и избежать частого написания сырых эффектов. Сообщество React поддерживает множество отличных пользовательских Hooks.
Предоставит ли React какое-либо встроенное решение для получения данных?
Мы все еще прорабатываем детали, но ожидаем, что в будущем вы будете писать выборку данных следующим образом:
1 2 3 4 5 6 7 |
|
Если вы используете в своем приложении пользовательские хуки, такие как useData
выше, то для перехода на рекомендуемый подход потребуется меньше изменений, чем если бы вы писали необработанные Эффекты в каждом компоненте вручную. Однако старый подход все еще будет работать, так что если вам нравится писать необработанные Эффекты, вы можете продолжать это делать.
Существует более одного способа сделать это¶
Допустим, вы хотите реализовать анимацию затухания с нуля, используя API браузера requestAnimationFrame
. Вы можете начать с Эффекта, который устанавливает цикл анимации. Во время каждого кадра анимации вы можете изменять непрозрачность узла DOM, который вы держите в ссылке, пока она не достигнет 1
. Ваш код может начинаться следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
|
Чтобы сделать компонент более читаемым, вы можете извлечь логику в пользовательский хук useFadeIn
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
Можно оставить код useFadeIn
как есть, но можно и рефакторить его. Например, вы можете извлечь логику установки анимационного цикла из useFadeIn
в пользовательский хук useAnimationLoop
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
Однако вы не обязаны это делать. Как и в случае с обычными функциями, в конечном итоге вы сами решаете, где проводить границы между различными частями вашего кода. Вы также можете использовать совершенно другой подход. Вместо того чтобы держать логику в Effect, вы можете перенести большую часть императивной логики внутрь JavaScript class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
Эффекты позволяют подключать React к внешним системам. Чем больше координации между эффектами требуется (например, для цепочки нескольких анимаций), тем больше смысла извлекать эту логику из эффектов и хуков полностью, как в песочнице выше. Тогда извлеченный вами код станет "внешней системой". Это позволяет вашим Эффектам оставаться простыми, потому что им нужно только посылать сообщения системе, которую вы перенесли за пределы React.
Приведенные выше примеры предполагают, что логика затухания должна быть написана на JavaScript. Однако эту конкретную анимацию затухания проще и гораздо эффективнее реализовать с помощью простой CSS-анимации:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Иногда даже не нужен Hook!
Итого
- Пользовательские хуки позволяют обмениваться логикой между компонентами.
- Имена пользовательских хуков должны начинаться с
use
и заканчиваться заглавной буквой. - Пользовательские хуки передают только логику состояния, но не само состояние.
- Вы можете передавать реактивные значения от одного хука к другому, и они остаются актуальными.
- Все хуки перезапускаются каждый раз, когда ваш компонент перерендеривается.
- Код ваших пользовательских хуков должен быть чистым, как и код вашего компонента.
- Оберните обработчики событий, получаемые пользовательскими хуками, в события Effect Events.
- Не создавайте пользовательские хуки типа
useMount
. Их назначение должно быть конкретным. - Вам решать, как и где выбирать границы вашего кода.
Задачи¶
1. Извлечение хука useCounter
¶
Этот компонент использует переменную состояния и Эффект для отображения числа, которое увеличивается каждую секунду. Извлеките эту логику в пользовательский хук под названием useCounter
. Ваша цель состоит в том, чтобы реализация компонента Counter
выглядела именно так:
1 2 3 4 |
|
Вам нужно будет написать свой пользовательский Hook в файле useCounter.js
и импортировать его в файл Counter.js
.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
1 |
|
Показать решение
Ваш код должен выглядеть следующим образом:
1 2 3 4 5 6 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Обратите внимание, что App.js
больше не нужно импортировать useState
или useEffect
.
2. Сделайте задержку счетчика настраиваемой¶
В этом примере есть переменная состояния delay
, управляемая ползунком, но ее значение не используется. Передайте значение delay
в ваш пользовательский хук useCounter
, и измените хук useCounter
, чтобы он использовал переданную delay
вместо жесткого кодирования 1000
мс.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Показать результат
Передайте задержку
вашему хуку с помощью useCounter(delay)
. Затем, внутри хука, используйте delay
вместо жестко заданного значения 1000
. Вам нужно будет добавить delay
в зависимости вашего Эффекта. Это гарантирует, что изменение delay
сбросит интервал.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
3. Извлечение useInterval
из useCounter
¶
В настоящее время ваш хук useCounter
делает две вещи. Он устанавливает интервал, а также увеличивает переменную состояния при каждом тике интервала. Выделите логику, которая устанавливает интервал, в отдельный хук под названием useInterval
. Он должен принимать два аргумента: обратный вызов onTick
и delay
. После этого изменения ваша реализация useCounter
должна выглядеть следующим образом:
1 2 3 4 5 6 7 |
|
Напишите useInterval
в файле useInterval.js
и импортируйте его в файл useCounter.js
.
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
1 |
|
Показать решение
Логика внутри useInterval
должна установить и очистить интервал. Больше ничего делать не нужно.
1 2 3 4 5 6 |
|
1 2 3 4 5 6 7 8 9 10 |
|
1 2 3 4 5 6 7 8 |
|
Обратите внимание, что в этом решении есть небольшая проблема, которую вы решите в следующей задаче.
4. Исправить интервал сброса¶
В этом примере есть два отдельных интервала.
Компонент App
вызывает useCounter
, который вызывает useInterval
для обновления счетчика каждую секунду. Но компонент App
также вызывает useInterval
для случайного обновления цвета фона страницы каждые две секунды.
По какой-то причине обратный вызов, обновляющий фон страницы, никогда не выполняется. Добавьте несколько журналов внутри useInterval
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Совпадают ли журналы с тем, что вы ожидаете? Если некоторые из ваших Эффектов, кажется, пересинхронизируются без необходимости, можете ли вы предположить, какая зависимость вызывает это? Есть ли способ удалить эту зависимость из вашего Эффекта?
После устранения проблемы, вы должны ожидать, что фон страницы будет обновляться каждые две секунды.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
1 2 3 4 5 6 7 8 9 10 |
|
1 2 3 4 5 6 7 8 9 10 11 |
|
Показать подсказку
Похоже, что ваш хук useInterval
принимает в качестве аргумента слушатель событий. Можете ли вы придумать, как обернуть этот слушатель событий так, чтобы он не был зависим от вашего Effect?
Показать решение
Внутри useInterval
оберните обратный вызов тика в событие эффекта, как вы делали ранее на этой странице.
Это позволит вам опустить onTick
из зависимостей вашего Эффекта. Эффект не будет пересинхронизироваться при каждом повторном рендере компонента, поэтому интервал изменения цвета фона страницы не будет сбрасываться каждую секунду, прежде чем успеет сработать.
Благодаря этому изменению оба интервала работают, как и ожидалось, и не мешают друг другу:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
1 2 3 4 5 6 7 8 9 10 |
|
1 2 3 4 5 6 7 8 9 10 |
|
5. Реализация шагающего движения¶
В этом примере хук usePointerPosition()
отслеживает текущую позицию указателя. Попробуйте переместить курсор или палец по области предварительного просмотра и увидите, как красная точка следует за вашим движением. Ее положение сохраняется в переменной pos1
.
На самом деле, в данный момент отображается пять (!) различных красных точек. Вы не видите их, потому что в настоящее время все они отображаются в одном и том же положении. Это то, что вам нужно исправить. Вместо этого вы хотите реализовать "ступенчатое" движение: каждая точка должна "следовать" по пути предыдущей точки. Например, если вы быстро перемещаете курсор, первая точка должна следовать за ним немедленно, вторая точка должна следовать за первой с небольшой задержкой, третья точка должна следовать за второй и так далее.
Вам необходимо реализовать пользовательский хук useDelayedValue
. Его текущая реализация возвращает предоставленное ему value
. Вместо этого вы хотите возвращать значение, полученное от delay
миллисекунды назад. Для этого вам может понадобиться некоторое состояние и Эффект.
После реализации useDelayedValue
, вы должны увидеть, как точки движутся друг за другом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Показать подсказку
Вам нужно будет хранить delayedValue
как переменную состояния внутри вашего пользовательского Hook. Когда value
изменится, вы захотите запустить Эффект. Этот Эффект должен обновить delayedValue
после delay
. Возможно, вам будет полезно вызвать setTimeout
.
Нужно ли очистить этот Эффект? Почему или почему нет?
Показать решение
Вот рабочая версия. Вы храните delayedValue
как переменную состояния. Когда value
обновляется, ваш Effect планирует таймаут для обновления delayedValue
. Вот почему delayedValue
всегда "отстает" от фактического value
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Обратите внимание, что этот Эффект не нуждается в очистке. Если бы вы вызвали clearTimeout
в функции очистки, то при каждом изменении value
сбрасывался бы уже запланированный таймаут. Чтобы движение было непрерывным, нужно, чтобы срабатывали все таймауты.
Источник — https://react.dev/learn/reusing-logic-with-custom-hooks