Анимация в приложениях React — популярная и обсуждаемая тема. Дело в том, что способов ее создания очень много. Некоторые разработчики используют CSS, добавляя теги в HTML-классы. Отличный способ, его стоит применять. Но, если вы хотите работать со сложными видами анимаций, стоит уделить время изучению GreenSock, это популярная и мощная платформа. Также для создания анимаций существует масса библиотек и компонентов. Давайте поговорим о них.

В статье рассматривается пять способов анимирования React-приложений:

  • CSS;
  • ReactTransitionGroup;
  • React-animations;
  • React-reveal;
  • TweenOne и Ant Design.

Skillbox рекомендует: Образовательный онлайн-курс «Профессия Java-разработчик».
Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».

Все примеры доступны в репозитории (отсюда в статью вставлены исходники вместо картинок, как в оригинальной статье).

CSS


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

В качестве примера — анимированное меню:



Оно относительно простое, со свойством CSS и триггером типа className = «is-nav-open» для тега HTML.

Использовать этот метод можно разными способами. Например, создать над навигацией wrapper, а затем вызывать изменения полей. Поскольку навигация имеет постоянную ширину, которая равна 250px, ширина wrapper со свойством margin-left или translateX должна иметь ту же ширину. При необходимости показать навигацию нужно добавить className = «is-nav-open» для wrapper и переместить wrapper на margin-left / translateX: 0;.

В конечном итоге исходник анимации будет выглядеть следующим образом:

export default class ExampleCss extends Component {
    handleClick() {
        const wrapper = document.getElementById('wrapper');
        wrapper.classList.toggle('is-nav-open')
    }
    render() {
        return (
            <div id="wrapper" className="wrapper">
                <div className="nav">
                    <icon
                        className="nav__icon"
                        type="menu-fold"
                        onClick={() => this.handleClick()}/>
                    <div className="nav__body">
                        Lorem ipsum dolor sit amet, consectetur adipisicing elit.
                        Beatae ducimus est laudantium libero nam optio repellat
                        sit unde voluptatum?
                    </div>
                </div>
            </div>
        );
    }
}

А вот CSS-стили:

.wrapper {
    display: flex;
    width: 100%;
    height: 100%;
    transition: margin .5s;
    margin: 0 0 0 -250px;
}
 
.wrapper.is-nav-open {
    margin-left: 0;
}
 
.nav {
    position: relative;
    width: 250px;
    height: 20px;
    padding: 20px;
    border-right: 1px solid #ccc;
}
.nav__icon {
    position: absolute;
    top: 0;
    right: -60px;
    padding: 20px;
    font-size: 20px;
    cursor: pointer;
    transition: color .3s;
}
 
.nav__icon:hover {
    color: #5eb2ff;
}

Повторюсь, если анимация относительно проста, то этот метод — основной. Пользователей порадует быстродействие браузера.

ReactTransitionGroup


Компонент ReactTransitionGroup разработала команда сообщества ReactJS. С его помощью можно без проблем реализовать основные CSS-анимации и переходы.

ReactTransitionGroup предназначен для изменения классов при изменении жизненного цикла компонента. У него небольшой размер, его нужно установить в пакете для React-приложения, что незначительно увеличит общий размер сборки. Кроме того, можно использовать и CDN.

У ReactTransitionGroup есть три элемента, это Transition, CSSTransition и TransitionGroup. Для запуска анимации в них нужно обернуть компонент. Стиль, в свою очередь, нужно прописывать в классах CSS.

Вот анимация, а дальше — способ ее реализации.



Первым делом нужно импортировать CSSTransitionGroup из react-transition-group. После этого требуется обернуть список и установить свойство transitionName. Каждый раз при добавлении или удалении дочернего элемента в CSSTransitionGroup он получает анимационные стили.

<CSSTransitionGroup
    transitionName="example">
    {items}
</CSSTransitionGroup>

При установке свойства transitionName = «example» классы в таблицах стилей должны начинаться с имени примера.

.example-eneter {
    opacity: 0.01;
}
 
.example-enter.example-enter-active {
    opacity: 1;
    transition: opacity 300ms ease-in;
}
 
.example-leave {
    opacity: 1;
}
 
.example-leave.example-leave-active {
    opacity: 0.01;
    transition: opacity 300ms ease-in;

Выше показан пример использования ReactTransitionGroup.

Нужна еще и логика, причем два метода для реализации примера добавления списка контактов.

Первый метод handleAdd — он добавляет новые контакты, получает случайное имя, которое затем помещает в массив state.items.

Для удаления контакта по индексу в массиве state.items используется handleRemove.

import React, { Component, Fragment } from 'react';
import { CSSTransitionGroup } from 'react-transition-group'
import random from 'random-name'
import Button from './button'
import Item from './item'
import './style.css';
export default class ReactTransitionGroup extends Component {
    
    constructor(props) {
        super(props);
        this.state = { items: ['Natividad Steen']};
        this.handleAdd = this.handleAdd.bind(this);
    }
 
    handleAdd() {
        let newItems = this.state.items;
        newItems.push(random());
        this.setState({ items: newItems });
    }
 
    render () {
        const items = this.state.items.map((item, i) => (
            <Item
            item={item}
            key={i}
            keyDelete={i}
            handleRemove={(i) => this.handleRemove(i)}
            />
        ));
 
    return (
        <Fragment>
            <Button onClick={this.handleAdd}/>
                <div className="project">
                    <CSSTransitionGroup
                    transitionName="example"
                    transitionEnterTimeout={500}
                    transitionLeaveTimeout={300}
                    >
                        {items}
                    </CSSTransitionGroup>
                </div>
        </Fragment>
    );
}
};

React-animations


React-animations представляет собой библиотеку, которая построена на animate.css. С ней просто работать, у нее множество разных коллекций анимации. Библиотека совместима с любой inline-style-библиотекой, поддерживающей использование объектов для определения основных кадров анимации, включая Radium, Aphrodite или styled-components.



Я знаю, что вы думаете:



Теперь проверим, как это работает на примере анимации подпрыгивания.



Первым делом импортируем анимацию из react-animations.

const Bounce = styled.div`animation: 2s ${keyframes`${bounce}`} infinite`;

Затем, после создания компонента, оборачиваем любой HTML-код или компонент для анимации.

<bounce><h1>Hello Animation Bounce</h1></bounce>

Пример:

import React, { Component } from 'react';
import styled, { keyframes } from 'styled-components';
import { bounce } from 'react-animations';
import './style.css';
 
const Bounce = styled.div`animation: 2s ${keyframes`${bounce}`} infinite`;
 
export default class ReactAnimations extends Component {
    render() {
        return (
            <Bounce><h1>Hello Animation Bounce</h1></bounce>
        );
    }
}

Все работает, анимация очень простая. Кроме того, есть отличное решение для использования анимации подпрыгивания при прокрутке — react-animate-on-scroll.

React-reveal


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

import Fade from 'react-reveal/Fade';
 
<Fade top>
    <h1>Title</h1>
</Fade>



Всего есть пять блоков, у каждого из них полноэкранная страница и заголовок.

import React, { Component, Fragment } from 'react';
import Fade from 'react-reveal/Fade';
 
const animateList = [1, 2, 3, 4, 5];
 
export default class ReactReveal extends Component {
    render() {
        return (
            <Fragment>
                {animateList.map((item, key) => (
                    <div style={styles.block} key={key}>
                        <Fade top>
                            <h1 style={styles.title}>{`block ${item}`}</h1>                       
                        </Fade>
                    </div>
                ))}
            </Fragment>
        );
    }
}
 
const styles = {
    block: {
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        width: '100%',
        height: '100%',
        background: '#000',
        borderBottom: '1px solid rgba(255,255,255,.2)',
    },
    title: {
        textAlign: 'center',
        fontSize: 100,
        color: '#fff',
        fontFamily: 'Lato, sans-serif',
        fontWeight: 100,
    },
};

Теперь вводим константу animateList. В массив включены пять элементов. После использования метода массива map есть возможность рендерить любой элемент в компонентах Fade, вставляя элементы в заголовок. Стили, которые определены в константе styles, получают короткие CSS-стили как для блока, так и для заголовка. Выше — пять блоков с анимацией Fade.

TweenOne и анимация в Ant Design


Ant Design — React UI-библиотека, которая содержит большое количество полезных и простых в использовании компонентов. Она подойдет, если вам нужно создавать элегантные пользовательские интерфейсы. Разработали ее в компании Alibaba, которая использует библиотеку во множестве своих проектов.



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



В анимации использован компонент TweenOne, которому нужен PathPlugin, чтобы правильно задать траекторию движения. Работать все это будет лишь в том случае, если поместить
PathPlugin в TweenOne.plugins.

TweenOne.plugins.push(PathPlugin);

Основными параметрами анимации являются следующие:

  • duration — время анимации в мс;
  • ease — плавность анимации;
  • yoyo — изменение движения вперёд и назад с при каждом повторении;
  • repeat — повтор анимации. Нужно использовать -1 для бесконечной анимации;
  • p — координаты пути для анимации;
  • easePath — координаты плавного пути для анимации.

Два последних параметра весьма специфичны, но о них не стоит беспокоиться, все работает, как нужно.

const duration = 7000;
const ease = 'easeInOutSine';
const p =
  'M123.5,89.5 C148,82.5 239.5,48.5 230,17.5 C220.5,-13.5 127,6 99.5,13.5 C72,21 -9.5,56.5 1.5,84.5 C12.5,112.5 99,96.5 123.5,89.5 Z';
const easePath =
  'M0,100 C7.33333333,89 14.3333333,81.6666667 21,78 C25.3601456,75.6019199 29.8706084,72.9026327 33,70 C37.0478723,66.2454406 39.3980801,62.0758689 42.5,57 C48,46.5 61.5,32.5 70,28 C77.5,23.5 81.5,20 86.5,16 C89.8333333,13.3333333 94.3333333,8 100,0';
const loop = {
  yoyo: true,
  repeat: -1,
  duration,
  ease,
};

Теперь можно приступать к созданию объекта анимации.

  • redSquare содержит параметры цикла плюс координату Y, длительность и задержку.
  • greenBall содержит путь с параметрами объекта x, у — значение p. Кроме того, длительность, повтор и плавность — функция TweenOne.easing.path, у которой два параметра.
  • path — easePath.
  • lengthPixel — кривая, которая разделена всего на 400 секций.
  • track — овал с осями, у него есть стили цикла и параметр поворота.

const animate = {
  redSquare: {
    ...loop,
    y: 15,
    duration: 3000,
    delay: 200,
  },
  greenBall: {
    path: { x: p, y: p },
    duration: 5000,
    repeat: -1,
    ease: TweenOne.easing.path(easePath, { lengthPixel: 400 }),
  },
  track: {
    ...loop,
    rotate: 15,
  },
};

Также необходимо обратить внимание на компонент TweenOne. Все компоненты будут импортированы из rc-tween-one. TweenOne — базовый компонент с базовыми же proprs и анимационными props, которые и представляют собой анимацию. У каждого TweenOne — собственные параметры анимации, такие, как redSquare, track, greenBall.

import React from 'react';
import TweenOne from 'rc-tween-one';
 
export default function BannerImage() {
    return (
      <div className="wrapper-ant-design">
        <svg width="482px" height="500px" viewBox="0 0 482 500">
          <defs>
            <path
              d="M151,55 C129.666667,62.6666667 116,74.3333333 110,90 C104,105.666667 103,118.5 107,128.5 L225.5,96 C219.833333,79 209.666667,67 195,60 C180.333333,53 165.666667,51.3333333 151,55 L137,0 L306.5,6.5 L306.5,156 L227,187.5 L61.5,191 C4.5,175 -12.6666667,147.833333 10,109.5 C32.6666667,71.1666667 75,34.6666667 137,0 L151,55 Z"
              id="mask"
            />
          </defs>
          <g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd" transform="translate(0, 30)">
            <g id="Group-13" transform="translate(0.000000, 41.000000)">
              <TweenOne component="g" animation={animate.redSquare}>
                <rect
                  stroke="#F5222D"
                  strokeWidth="1.6"
                  transform="translate(184.000000, 18.000000) rotate(8.000000) translate(-184.000000, -18.000000) "
                  x="176.8"
                  y="150.8"
                  width="14.4"
                  height="14.4"
                  rx="3.6"
                />
              </TweenOne>
            </g>
            <g id="Group-14" transform="translate(150.000000, 230.000000)">
              <g id="Group-22" transform="translate(62.000000, 7.000000)">
                <image
                  id="cc4"
                  alt="globe"
                  xlinkHref="https://gw.alipayobjects.com/zos/rmsportal/FpKOqFadwoFFIZFExjaf.png"
                  width="151px"
                  height="234px"
                />
              </g>
              <mask id="mask-2">
                <use xlinkHref="#mask" fill="white" transform="translate(-42, -33)" />
              </mask>
              <g mask="url(#mask-2)">
                <TweenOne component="g" animation={animate.track} style={{ transformOrigin: '122.7px 58px' }}>
                  <g transform="translate(-16, -52)">
                    <g transform="translate(16, 52)">
                      <path
                        d="M83.1700911,35.9320015 C63.5256194,37.9279025 44.419492,43.1766434 25.8517088,51.6782243 C14.3939956,57.7126276 7.77167019,64.8449292 7.77167019,72.4866248 C7.77167019,94.1920145 61.1993389,111.787709 127.105708,111.787709 C193.012078,111.787709 246.439746,94.1920145 246.439746,72.4866248 C246.439746,55.2822262 212.872939,40.6598106 166.13127,35.3351955"
                        id="line-s"
                        stroke="#0D1A26"
                        strokeWidth="1.35"
                        strokeLinecap="round"
                        transform="translate(127.105708, 73.561453) rotate(-16.000000) translate(-127.105708, -73.561453) "
                      />
                    </g>
                    <TweenOne component="g" animation={animate.greenBall}>
                      <image
                        alt="globe"
                        id="id2"
                        xlinkHref="https://gw.alipayobjects.com/zos/rmsportal/IauKICnGjGnotJBEyCRK.png"
                        x="16"
                        y="62"
                        width="26px"
                        height="26px"
                      />
                    </TweenOne>
                  </g>
                </TweenOne>
              </g>
            </g>
          </g>
        </svg>
      </div>
    );
  }



Да, выглядит страшновато, но анимация с использованием этого метода проста.

 <TweenOne component="g" animation={animate.redSquare} />
  <TweenOne component="g" animation={animate.track} />
  <TweenOne component="g" animation={animate.greenBall} />

Требуется всего лишь описать правила анимации и перенести их в компонент TweenOne.

Для достижения разных целей нужны разные подходы. В этой статье были рассмотрены несколько решений, которые можно использовать в большом количестве проекта. Ваше дело — выбрать подходящее.
Skillbox рекомендует:

Комментарии (14)


  1. AxisPod
    20.06.2019 18:15
    +3

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


    1. Moxa
      20.06.2019 18:29
      +1

      а что не так с первым примером?


      1. kahi4
        20.06.2019 18:31
        +1

        Настолько не-реакт-вей ещё поискать нужно, конечно.


        1. Moxa
          20.06.2019 18:34
          +1

          не реакт-вей там только classList.toggle, допустим вместо него есть локальный стейт компонента, относительно которого в рендере ставится нужный css-класс


          1. kahi4
            20.06.2019 18:37
            +2

            Ну раз мы и говорим про анимации в реакте, наверное и нужно делать как принято в реакте, с стейтом, да ключем в render? Да и конкретно в данном примере document.getElementById выглядит чужеродно, в крайнем случае есть refs


      1. Koneru
        20.06.2019 19:00
        +1

        Я думаю AxisPod намекал на ref и на state, если не так, то я про это говорю. Оба подхода работают, но предложенный вариант хуже работает с жизненным циклом компонента.


      1. vanxant
        20.06.2019 21:36

        Ну кроме jquery-way ещё то, что там нет собственно анимаций, а есть только переходы (transition).


        1. Moxa
          20.06.2019 21:54

          что мешает повесить keyframes анимацию на класс?


          1. vanxant
            20.06.2019 22:29

            Вот и я не знаю, что мешало повесить именно анимацию, а не унылый транзишен на margin :) Да хотя бы и из того же animate.css, раз он вызвал такой вау-эффект у автора.


    1. kahi4
      20.06.2019 18:35

      Второй пример тоже никто ни разу не запускал. Был бы отличным примером, почему нельзя индекс из итератора как ключ (key) использовать (потому что при удалении из середины анимация проигрываться не будет), а ещё забыли метод handleDelete,


      1. kahi4
        20.06.2019 19:23

        И данный пример устарел, покуда АПИ библиотеки значительно поменялось.


  1. faiwer
    20.06.2019 20:42

    handleClick() {
        const wrapper = document.getElementById('wrapper');
        wrapper.classList.toggle('is-nav-open')
    }

    o_O. Как это попало на главную? Зачем эта статья здесь, да ещё и с таким клик-бейт заголовком. Дальше можно даже не читать. Кажется автор видел React только на картинках через подзорную трубу.


  1. alex_blank
    21.06.2019 06:10

    > добавляя теги в HTML-классы


    WTF am I reading?


  1. mayorovp
    21.06.2019 14:30

    const wrapper = document.getElementById('wrapper');

    Никогда не пишите вот так в Реакте! тут проблема даже не в нарушении какого-то там react-way, тут нарушается сам принцип выделения компонентов! Каждый компонент должен быть переиспользуемым и разумно изолированным. Здесь же не наблюдается ни того, ни другого...


    Если уж так нужно "поиграть" с элементами DOM — используйте предназначенные для этого инструменты! Неужели так сложно было написать вот так?


    const wrapper = ReactDOM.findDOMNode(this);

    Или так?


    const wrapper = this.wrapperRef.current;