Анимация при наведении – прекрасный способ сделать приложение динамичным и отзывчивым. Это мелочь, но именно такие детали в итоге могут сделать продукт классным. Хотя порой простого изменения состояния по щелчку мыши бывает недостаточно. Вот пример того, что я имею ввиду. Может быть, всё дело в асимметрии, но что-то в этой анимации мне не нравится 😬 А что, если иконки в состоянии наведения будут поворачиваться только на какое-то время? Этот эффект мне нравится! Он динамический и неожиданный. Поскольку он гораздо сложнее обычного transition, делается он особым образом. Его можно применять довольно изощрённо, например... По результатам неофициального опроса в Twitter poll было решено назвать этот эффект «boop». В этом уроке для intermediate-пользователей React мы разберём, как его реализовать✨ В этом уроке мы пойдем по весьма извилистому пути. Мы рассмотрим различные паттерны архитектуры React и научимся комбинировать хуки с компонентами для максимального переиспользования. Надеюсь, вам понравится! Если не терпится взглянуть на код, это можно сделать здесь. Самое чудное в компонентно-ориентированных фреймворках, как React — возможность упаковывать поведение практически так же, как элементы пользовательского интерфейса. Кроме элементов <Button> и  <Table> можно создавать элементы <FadeIn> и <SoundEffect>. Взять наш пример. Эффект – кратковременная активация и ее отмена – может быть отделён от любого конкретного элемента пользовательского интерфейса, так что его можно применять с чем угодно. Первый взгляд на React-компонент: Тут много кода, так что давайте разбираться. Главная идея в том, что при наведении на элемент он переходит в другое состояние, как при использовании обычного hover-транзишна. Кроме этого, запускается таймер, и по истечении времени, состояние переключается обратно в «исходное» независимо от того, продолжаем мы наводить курсор на элемент или нет. Получается что-то вроде тех машин, которые сами себя отключают: Мы следим за состоянием boop с помощью стейта isBooped. Мы обертываем то, на что наложим boop, т.е. children, в span. Это нужно, чтобы применить стиль вращения и обработать события мыши, активирующие эффект.  Небольшое отступление Обычно лучшей практикой считается ставить обработчики событий на интерактивные элементы — кнопки или поля ввода. Пользователи, работающие на клавиатуре, не смогут установить фокус на div или span. Но сейчас особый случай. На самом деле я не хочу, чтобы этот эффект активировался при наведении фокуса. Он чисто декоративный и мне кажется, что при навигации с клавиатуры он будет только раздражать пользователей, состояние выделения для которых и так уже совершенно очевидно. Используем хук эффекта, который будет срабатывать при каждом изменении isBooped. Событие наведения приводит к переключению этого значения, которое приводит к срабатыванию хука эффекта. Эффект задает задержку, по истечении которой isBooped вновь переключается на значение false. Как насчёт самого эффекта? Пока что это только вращение. Когда isBooped истинно, мы применяем transform: rotate к оборачивающему элементу. В разных ситуациях могут быть нужны разные эффекты, поэтому изменять угол вращения и длительность транзишна будем через пропсы. Кроме того, нам необходимо настроить display на inline-block, потому что элементы inline не трансформируемые. Также добавляем backface-visibility: hidden, чтобы воспользоваться преимуществами аппаратного ускорения*. * Технически, эта возможность влияет на то, как наш элемент будет выглядеть, повернувшись к пользователю «спиной». Нам это неважно, зато эта возможность имеет побочный эффект: заставляет рендерить на GPU, что делает анимацию более «гладкой». Новый компонент Boop используем следующим образом: Выглядит он так... Неплохо, неплохо. Но мы способны на большее. Движение этой начальной версии кажется механическим и неестественным. В ней нет ни плавности, ни естественности движений, которые мы ждем от современной анимации. В статье «Знакомство со Spring Physics» рассказано, как придать глубину и реалистичность своим анимациям. ← Советую посмотреть, там представлены две такие забавные пружинистые демки Моя любимая библиотека spring-physics анимации — это ReactSpring. Она предоставляет современное API, основанную на хуках, и непревзойденную производительность.  Давайте подправим наш код, чтобы использовать её вместо переходов CSS транзишнов: Прежде мы создавали объект style и передавали напрямую в наш span. Теперь же мы передаем этот объект стиля (без transition) в useSpring. Хук useSpring можно сравнить с конвейерным станком, который начиняет пирожное клубничным джемом. Иначе говоря, он берет немного обычного CSS и начиняет его ✨пружинной магией ✨. Вместо использования кривых Bézier из CSS, он будет использовать математику пружины. Вот почему мы опускаем свойства transition: мы делегируем эту задачу ReactSpring. Ввиду того, что springphysics пока не сильно распространен в интернете и не является стандартом, мы не можем передать этот начиненный магией стиль объекта на <span>. Поэтому мы запускаем <animated.span>, который идентичен прежнему и дополнительно знает, как обращаться с созданным “пружинным” объектом стиля. Вот результат. Кажется, немного вяло. Давайте немного изменим  конфиг спринга: Взводя натяжение и снижая сопротивление, мы значительно смягчаем реакцию наших значков на наведение курсора. Вот это уже что-то! Почему именно задержка? Некоторые писали, что вместо использования setTimeout можно использовать колбэк onRest с APIReactSpring. Казалось бы, гораздо логичнее «отбипнуть», опираясь на саму анимацию, а не на какую-то произвольную величину времени.На самом деле в этом случае я хочу использовать именно задержку. Причин две: 1. Одно из классных преимуществ spring physics — то, как она грациозно обрабатывает прерывания. Я хочу прервать ее, чтобы отдернуть в исходное положение прежде, чем она вернется в состояние полного покоя. 2. Основная проблема с onRest в том, что он ждет, пока анимация полностью не остановится. Обычно это происходит гораздо позже воспринимаемой остановки, поскольку часто на кончике анимации бывает множество субпиксельных колебаний. Их, конечно, можно скорректировать через свойства конфигурации precision, но порой эти субпиксельные колебания придают приятный утонченный эффект. Было бы здорово, если бы у ReactSpring был еще такой API, который позволил бы задавать процент выполнения так, чтобы я мог сказать: «Активируй этот возврат, когда анимация будет выполнена на 50%». Но на самом деле решение с задержкой, к которому я прибегнул, нисколько не хуже 😄 Итак, пока что мы ограничили воздействие нашего boop на вращение, но это далеко не предел. Давайте доработаем его так, чтобы менять размеры с помощью scale и смещать положение с помощью translate. Добавим в transform эти свойства: Выставляем все значения по умолчанию для обычного состояния (например, 0pxtranslate, 1xscale). Это позволяет нам указывать в пропсах только те значения, которые мы собираемся менять: если мы не передадим значение для вращения, вращения не будет. Этот результат меня вполне устраивает, однако у этого решения есть одна серьезная проблемка. Вообще-то, нам придется полностью изменить подход! В одном проекте, над которым я сейчас работаю, есть виджеты, которые могут разворачиваться для полного отображения контента.  Я подумал, было бы здорово, если бы стрелка подскакивала при наведении на нее курсора. Это интересная и непростая задача, потому что здесь присутствует разделение – я хочу, чтобы бип оказывал воздействие только на стрелку, но при этом хочу, чтобы она реагировала на наведение на любую часть элемента. Если я проведу курсор над словом «Show», стрелка должна дернуться. Наш текущий подход такой возможности не дает от слова «совсем». Анимация привязана к тому же элементу, что и обработчик событий. Немного поэкспериментировав, я пришел к выводу, что правильным API для этого эффекта будут не компоненты, а хуки.  Давайте посмотрим на всё с точки зрения пользователя.  Я позже решу, как это внедрить; прежде всего необходимо придумать самый простой и легкий интерфейс. Вот, что у меня получилось: Нам необходима возможность передачи нашему хуку объекта, представляющего конфигурацию, который должен предоставить две вещи: Объект стиля, применяемый animated-элементам, например animated.span или animated.button; Функцию запуска, для вызова ее всякий раз, как нам понадобится boop. Если очень захочется, то мы можем применить и то и другое к одному элементу, но делать так необязательно.  Этот хук дает невероятную гибкость: его можно вызывать когда угодно и не только при наведении. К примеру, мы можем учесть пользователей мобильных телефонов, настроив действие эффекта по тапу или задать интервал срабатывания, чтобы выделить важную часть пользовательского интерфейса! * * Вот как это реализуется: Большая часть логики просто копируется; мы полностью повторяем действия при создании этого style-объекта. Но вместо применения его на элементе, мы просто возвращаем его из хука. Вот еще пара фишек: Конфигурация спринга теперь предоставляется как параметр, поскольку различные ситуации могут иметь разную физику. Функция пуска оборачивается в React.useCallback. Это нужно, чтобы ссылка на функцию не менялась на каждый  рендер, чтобы не допустить перерендера компонентов, обернутых в  memo. Ведь  мы не знаем, как будет использоваться функция запуска. Этот хук чудесен, но мне, правда, очень понравился тот компонентный API, о котором мы говорили до этого. Можем ли мы воспользоваться компонентом в тех случаях, когда не требуется разделение между обработчиком событий и анимацией? Что действительно круто в паттернах, так это то, что мы легко можем обернуть хук в компонент, чтобы сделать свою пироженку и съесть ее: Наш Boop-компонент стал намного меньше, потому что мы делегировали всю тяжелую работу хуку useBoop.Теперь у нас есть доступ к двум превосходным API, которые действуют на основе одной логики. DRY AF. В зависимости от вида анимации, иногда вы сможете заметить, что при завершении она как будто «встает на место». Это легкое смещение на один-два пикселя. Оно особенно распространено при анимации элемента, содержащего текст: Вы можете узнать больше, почему это происходит, и как это поправить: «Интерактивное руководство по CSS-переходам». Комбинация «компонент/хук»,  что мы создали, очаровательна, но очарование – вещь субъективная. Не  все хотят видеть скачущий UI, особенно люди с расстройствами вестибулярного аппарата. Я как-то уже писал, как создавать удобные анимации в React. Давайте применим эти знания и здесь: Хук prefers-reduced-motion сообщит нам, если пользователь захочет удалить движение. Когда это значение true, мы возвращаем «пустой» объект стиля. Учитывая, что объект стиля всегда пустой, мы будем уверены, что элемент не сдвинется. Прежде всего – спасибо, что дочитали до этого места! Это было непростое путешествие😄 Держите итоговую версию, чтобы скопировать и вставить в свой репозиторий:  Среди демок, показанных в начале, была звездообразная анимация. Этот эффект действительно выполнен с использованием хука useBoop, который мы создали, но в нем также задействована тригонометрия, рассмотрение которой не укладывается в рамки данного туториала. Сейчас я нахожусь в процессе написания поста о том, как использовать тригонометрию для создания эффектов, подобных этому.  Пока же поделюсь кодом, максимально расписав контекст в комментариях! Надеюсь, это поможет.🌟  Другие статьи про frontend для начинающих: Как сделать свой текстовый редактор на React.js Как работают браузеры: навигация и получение данных, парсинг и выполнение JS, деревья спец возможностей и рендеринга Другие статьи про frontend для продвинутых: Как мы выбирали архитектуру микрофронтендов в ЛК для 260 000 сотрудников Пятёрочки Как мы сетапили монорепозиторий с SSR и SPA для Otus.ru Как собрать свою библиотеку для SSR на React  редактор