`react-spring`を使ってReactでのアニメーションを行う

この記事は、[株式会社エイチームフィナジーの Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ateam-finergy) 20 日目の記事になります。
今回は @okkuyama が担当いたします。

## はじめに
Reactでフロントエンド実装でよりUI/UXを高めて行きたい場合アニメーション効果はわりと必要不可欠な存在かと思います。
ただ、アニメーションの実装はエンジニアかデザイナーどちらの領域かの線引きが難しく、かつ実装難易度も高めと感じています。
またアニメーション方法も複数種類存在していて、かつそれぞれの背景も異なることも実装のハードルを上げています。

## Reactでのアニメーション実装種類

### CSSを使ったアニメーション

アニメーション実装をする場合、HTML+CSSを経験された方が最初に思い描くのはCSSの`transition`や`animation`を使った実装かと思います。
もちろんReactでもこの実装方法は可能で主な実装方法は、予めアニメーション定義したCSSのclassを定義しておいて、ユーザー操作をトリガーに定義済みclassを付け外したりして動作させるイメージになります。
メリットは、過去にCSSアニメーションの経験がある方であればすぐに実装可能な方法かと思います。

### React用ライブラリを利用したアニメーション

ReactではCSSなどの知識がなくとも、簡単かつ柔軟にアニメーションを行うためのライブラリが揃っています。

メジャーなものは以下のものがあります。
- [react-spring](https://react-spring.io/)
- [Framer Motion](https://www.framer.com/motion/)
- [React Transition Group](https://reactcommunity.org/react-transition-group/)
- [React-Motion](https://github.com/chenglou/react-motion)
- [React Move](https://github.com/sghall/react-move)

今回はこの中で人気が高く、最近のアーキテクチャを採用した`react-spring`を使ったアニメーション実装方法について記述していきたいと思います。

## `react-spring` について

[react-spring](https://react-spring.io/)のサイトの説明によると、このライブラリは物理ベースのアニメーションライブラリで、このライブラリが登場する以前のメジャーアニメーションライブラリの`React-Motion`を踏襲をしているので、こちらのライブラリに慣れ親しんだ人も簡単に移行できるようです。

### 従来のアニメーション手法との違い

react-springの説明で「物理ベースのアニメーション」と出てきましたが、こちらは従来のアニメーションとどう違うのでしょうか？この違いを理解することがreact-springを学ぶ近道になるかと思います。

従来のアニメーションは`継続時間`と`イージング曲線`を設定する手法でした。

- `継続時間`: アニメーションの開始から終了までの時間を設定
- `イージング曲線`: より自然なアニメーションを行うための変化割合を設定（例として、はじめゆっくりで徐々に早くなる、はじめ早く終了に近づくと遅くなる、などがあります）

### 従来のアニメーションの問題点
この従来型でも十分アニメーション効果は得られるのですが、インタラクティブなフロントエンドで実装しようとすると以下のような問題点が出てきます。

アニメーションする時間やアニメーションの動きは予め決められたものになるため、ユーザーがどのような操作を行ったとしても常に同じ動きになる。
例えば、フリック動作などでユーザーが素早く操作を行ったとしてもアニメーションは常に一定となりインタラクティブ性が薄れてしまうようになります。

### react-springは物理ベースでのアニメーション
react-springには基本的には継続時間やイージング曲線の概念はありません。（※設定次第で従来型のアニメーションの指定も可能です）
設定で必要なのは以下のような物理属性です。（[config参照](https://react-spring.io/common/configs#configs)）
- **mass**: 質量
- **tension**: エネルギー負荷
- **friction**: 抵抗
- **precision**: 精度
- **velocity**: 速度
- ...他

上記のような物理属性と変化後の値を設定することであとはreact-springが物理演算でアニメーションを演出してくれます。
この設定値からわかる通り、早い動きには素早く動作し、ゆっくりした動作にはゆっくりと動作するアニメーションになるため、ユーザーの操作に対していよりインタラクティブな動きを実現できるようになります。

細かく設定するのが億劫な場合は便利なプリセットもありますので、こだわらずにそれなりのアニメーション効果を得たい場合は[こちらのpresets](https://react-spring.io/common/configs#presets)を利用するのもありかと思います。

ただ、物理ベースアニメーションは時間の概念がないため紙芝居ムービーのようなアニメーションには向かなくなりますが、その場合、継続時間とイージング曲線をプロパティで渡して従来型アニメーションとして動作させることも可能です。

## 実装方法
それではreact-springを使ったアニメーションの実装方法について説明していきます。
react-springでは大まかに2種類の実装方法があります。
- **Hooks型**
- **RenderProps型**
最近の主流はHooksかと思いますので今回は**Hooks型**での実装方法について述べていきます。

### 基本的なアニメーション実装
基本のアニメーション実装は`useSpring`というhooksを利用して実装します。
以下はコンポーネントを読み込むと同時にコンポーネント自体をフェードインさせるアニメーションの実装となります。

```jsx
import React, { useState, useEffect } from 'react'
import { useSpring, animated } from 'react-spring'

export const FadeInSample = () => {
  const [toggle, setToggle] = useState(false)
  const styles = useSpring({ opacity: toggle ? 1 : 0 })

  const handleToggle = () => {
    setToggle(_toggle => !_toggle)
  }

  useEffect(() => {
    handleToggle()
  }, [])

  return (
    <animated.div style={styles}>
      <p>Fade in text</p>
    </animated.div>
  )
}
```

<!-- TODO: アニメーション実行結果画面gif貼り付け -->

**実装内容説明**

- `useSpring`を使いstateの`toggle`変数の状態で透過率(opacity)が0←→1で変化するアニメーションスタイルを定義。
- アニメーションレンダリング可能な`<animated.div>`コンポーネントにuseSpringで作成したアニメーションスタイルを属性値として渡してアニメーションさせる。


#### リファクタリングしてもう少し簡略化

上記のサンプルは`toggle`という状態（state）をコンポーネントで持ち、その状態でアニメーションを切り替えています。この`toggle`がアニメーション以外でも利用するのであればこの実装でもよいのですが、アニメーションのためだけに`useState`でわざわざ状態管理するのは冗長的な場合以下のような実装も可能です。

```jsx
import React, { useState, useEffect } from 'react'
import { useSpring, animated } from 'react-spring'

export const FadeInSample = () => {
  const [styles, setStyles] = useSpring(() => { opacity: 0 })

  useEffect(() => {
    setStyles({ opacity: 1 })
  }, [])

  return (
    <animated.div style={styles}>
      <p>Fade in text</p>
    </animated.div>
  )
}
```

前回との違い`useSpring`の定義の仕方が違うようになります。

- 前回の方式：アニメーションスタイルのみ受け取る

  ```jsx
  const styles = useSpring({ opacity: toggle ? 1 : 0 })
  ```

- 今回の方式：アニメーションスタイルと、設定メソッドを受け取る

  ```jsx
  const [styles, setStyles] = useSpring(() => { opacity: 0 })
  ```
※ 後者の定義は`useSpring`に渡すのがスタイルオブジェクトそのものではなく、スタイルオブジェクトを返す関数を渡していることに注意してください。


### ユーザー操作をトリガーとしたアニメーション
ユーザーのボタン操作などでアニメーションを行う方法。
この場合はFade In時とFade Out時の状態を明示的に保つ必要があるため

```jsx
import React, { useState, useEffect } from 'react'
import { useSpring, animated } from 'react-spring'

export const FadeInSample = () => {
  const [toggle, setToggle] = useState(false)
  const styles = useSpring({ opacity: toggle ? 1 : 0 })

  const handleToggle = () => {
    setToggle(_toggle => !_toggle)
  }

  return (
    <>
      <button onClick={handleToggle}>{toggle ? 'Fade Out' : 'Fade IN'}</button>
      <animated.div style={styles}>
        <p>Fade in text</p>
      </animated.div>
    </>
  )
}
```


### アニメーション完了をトリガーしたい場合
アニメーションの完了を待ってからなにか処理を行いたい場合もあるかと思います。その場合は以下のような実装となります。

```jsx
import React, { useState, useEffect } from 'react'
import { useSpring, animated } from 'react-spring'

export const FadeInSample = () => {
  const [styles, setStyles] = useSpring(() => { opacity: 0 })

  useEffect(() => {
    setStyles({
      opacity: 1,
      onRest: () => {
        // アニメーション終了後の処理
      }
    })
  }, [])

  return (
    <animated.div style={styles}>
      <p>Fade in text</p>
    </animated.div>
  )
}
```

**実装内容説明**
`onRest`を使いアニメーション終了後の処理をコールバックで定義します。


### ページ移動などのトランジションを行う
Next.jsなど複数ページにまたがるSPA実装の場合、ページ移動時にトランジションアニメーションを利用したいシーンなどがあるかと思います。この場合も`react-spring`を使えばわりと簡単に実装が可能です。

```jsx
import React, { useState } from 'react'
import { useTransition, animated, config } from 'react-spring'

// 遷移させるコンポーネント
const Component1 = () => <animated.div>Component1</animated.div>
const Component2 = () => <animated.div>Component2</animated.div>
const Component3 = () => <animated.div>Component3</animated.div>

export const Transition = () => {
  const [pageIndex, setPageIndex] = useState(0)
  // トランジションで切り替えするためコンポーネントを配列格納する
  const componentList = [Component1, Component2, Component3]

  // トランジション遷移定義
  const transitions = useTransition(pageIndex, item => item, {
    unique: true,
    from: { opacity: 0 },
    enter: { opacity: 1 },
    leave: { opacity: 0 },
    config: config.molasses,
  })

  const handleTransition = () => {
    setPageIndex(page => (page+1) % componentList.length)
  }

  return (
    <div onClick={handleTransition}>
      {transitions.map(({item, props, key}) => {
        const Component = componentList[item]
        return <Component key={key} />
      })}
    </div>
  )
}
```


### 従来型の継続時間とイージング曲線を使ったアニメーション
時間軸で紙芝居型のアニメーションが必要なケースもあるかと思います。その場合はuseSpringに渡す引数を変えることで実現可能となります。

従来型アニメーションを行う場合の注意点は、`useSpring`に渡す引数を`duration`と`easing`のみにすることです。物理ベースの引数(mass, tension, friction, precision, velocityなど)を渡すと物理ベースのアニメーションが優先となるのでご注意ください。

※ このサンプルではイージング曲線ライブラリの[d3-ease](https://github.com/d3/d3-ease)を使っていますが、自前で関数定義を行っても設定できます。

```jsx
import React from 'react'
import { useSpring, animated } from 'react-spring'

export const TimeLineAnimation = () => {
  const animation = {
    config: {
      duration: 2000,
      easing: d3.easeLinear(1),
    },
    from: {
      transform: 'translateX(-300px)',
    },
    to: {
      transform: 'translateX(0px)',
    },
  }
  const [styles] = useSpring(animation)

  return (
    <animated.div style={styles}>
      <p>Slide in text</p>
    </animated.div>
  )
}
```

### その他
react-springでは`useSpring`や`useTransiton`以外にも以下のhooksがあるので用途に合わせて使ってみてください。

- **useSprings** : 複数のアニメーションの作成
- **useChain** : 複数のアニメーションをつなげ、連続したアニメーションを行う
- **useTrail** : 複数のアニメーションを作成、各アニメーションは前のアニメーションに従って連続して動作します

## まとめ
react-springはとっつきづらいアニメーションにあって、低コストに学習でき非常にパワフルかつインタラクティブなUI/UXを提供できるアニメーションライブラリかと思います。
ユーザーの入力値に応じて、より早ければより早く、ゆっくりであればゆっくりと動作するため、気持ちの良いユーザー体験を実現できるのではないでしょうか。

また、Next.jsなどのSPAライブラリと組み合わせも可能なのも魅力かと思います。

プリセット値を使うことで特に凝った設定を行わずとも、簡単な設定値でアニメーション実現も可能なのでとりあえずデフォルト値で組んでおいて、後ほど細かく物理属性を煮詰め治すことも可能なので、アニメーションの最終調整を分業して行うなども可能になります。

react-springのアニメーションの概念を理解してしまえば簡単に実装できるので、これを機にサクッと実装してみてはどうでしょうか？素敵なユーザー体験を実現してDAU向上されればと思います。