7839

雑草魂エンジニアブログ

【React】ReactでCSSアニメーション(react-transition-group の利用)

最近は、UIフレームワークばかりを使っていて、独自でアニメーションを実装する機会がなかったが、React で簡単なタブ切り替えのスライドイン・アウトを実装する機会があったので、React での CSS アニメーション実装に関して備忘録を残しておく。

react-transition-group

reactcommunity.org

本ライブラリは、React の公式でも紹介されている、React で CSSアニメーションを実装するためのライブラリである。ただし、スライドイン・アウトなどのアニメーション、そのものは提供されておらず、CSSアニメーションは自分たちで実装する必要がある。CSSアニメーションを適切なタイミングでDOMに適応させる管理手法を本ライブラリでは提供してくれている。

Vue の場合、transition ラッパーコンポーネントがデフォルトで提供されているが、まさに同じようなライブラリである。

jp.vuejs.org

本ライブラリには、4つのラッパーコンポーネントが存在する。

  1. Transition
  2. CSSTransition
  3. TransitionGroup(Transition or CSSTransition のラッパーコンポーネント
  4. SwitchTransition(Transition or CSSTransition のラッパーコンポーネント

インストール方法

# npm
npm install react-transition-group --save
# yarn
yarn add react-transition-group

1. Transition

Transitionは、<Transition> の Props inの変化と時間 timeout の経過に応じて、stateが以下の4つのstateに変化し、子コンポーネントを再レンダリングする。子コンポーネントでは、4つのstateを受け取り、それぞれの css スタイルを用意しておくことで、それぞれのタイミングで適応させることができる。

state inの変化によるCSS適応タイミング
entering false→trueの変更時
entered false→trueになり、timeout経過後
exiting true→falseの変更時
exited true→falseになり、timeout経過後

また、各タイミングごとに実行されるコールバック関数も設定することができる。(以下では、enterのみ記載しているが、exitも同様に存在する。)

  • onEnter:entering適用前
  • onEntering:entering適用後
  • onEntered:entered適用後

以下の例では、inPropの変化に応じて、フェードイン/アウトを行っている。cssのtransitionとtimeoutの設定で様々な表現が実現可能となる。

f:id:serip39:20201002123301g:plain

import React, { useState } from 'react'
import { Transition } from 'react-transition-group'
const page = () => {
  const [inProp, setInProp] = useState(false)
  const transitionStyles = {
    entering: { opacity: 1, color: 'red', transition: 'all 1s ease' },
    entered: { opacity: 1, color: 'blue' },
    exiting: { opacity: 0, transition: 'all 1s ease' },
    exited: { opacity: 0 },
  }
  return (
    <>
      <Transition in={inProp} timeout={1500}>
        {(state) => (
          <div style={transitionStyles[state]}>
            <p>React CSS Animation</p>
          </div>
        )}
      </Transition>
      <button style={{ marginTop: '10px' }} onClick={() => setInProp(!inProp)}>
        Click
      </button>
    </>
  )
}
export default page

2. CSSTransition

CSSTransition は、Transition の拡張版のコンポーネントである。公式には、Angular.js の ngAnimate ライブラリにインスパイヤされたコンポーネントと書いてある。CSSトランジションやアニメーションを使う場合は、このコンポーネントを使用する方が簡単に様々な実装が可能となる。CSSTransitionでは、状態遷移中、開始および終了の状態中にクラス名を適用する。

f:id:serip39:20201002132552p:plain

-appear も存在して、inの変化ではなく、マウント時に適用される。注意点として、in={false} でマウントした場合は、何もクラスが適応されない。(-exit-done が適応される気がしたが違うので、注意とのこと。アニメーション終了後に、-done が適用される。)

また、in={false} の時にコンポーネントをアンマウントにしたい場合は、unmountOnExit={true}を指定する。以下の例では、Transitionと同じようにフェードイン/アウトを行っているが、 unmountOnExit を指定しないと、マウント時にはクラスが適応されないので、クラスが何も適応されていないpタグがマウントされ、文字が普通に表示されるようになってしまう。

f:id:serip39:20201002142557g:plain

import React, { useState } from 'react'
import { CSSTransition } from 'react-transition-group'
import styled from 'styled-components'
const page = () => {
  const [inProp, setInProp] = useState(false)
  return (
    <Root>
      <CSSTransition in={inProp} timeout={1500} classNames="fade" unmountOnExit>
        <p>React CSS Animation</p>
      </CSSTransition>
      <button style={{ marginTop: '10px' }} onClick={() => setInProp(!inProp)}>
        Click
      </button>
    </Root>
  )
}
const Root = styled.div`
  .fade-enter {
    opacity: 0;
  }
  .fade-enter-active {
    transition: all 1s ease;
    color: red;
    opacity: 1;
  }
  .fade-enter-done {
    color: blue;
    opacity: 1;
  }
  .fade-exit {
    opacity: 1;
  }
  .fade-exit-active {
    transition: all 1s ease;
    opacity: 0;
  }
`
export default page

3. TransitionGroup

TransitionGroup は、Transition または CSSTransition のリストを管理する為のコンポーネントである。これを使うことで、タブ切り替えなどを実装することができる。

f:id:serip39:20201002151209g:plain

import React, { useState } from 'react'
import { TransitionGroup, CSSTransition } from 'react-transition-group'
import styled from 'styled-components'
const page = () => {
  const [tabIndex, setTabIndex] = useState(0)
  const tabs = [
    { index: 0, content: 'content 0' },
    { index: 1, content: 'content 1' },
    { index: 2, content: 'content 2' },
    { index: 3, content: 'content 3' },
  ]
  return (
    <Root>
      <div className="tabs">
        {tabs.map((tab) => (
          <button key={tab.index} onClick={() => setTabIndex(tab.index)}>
            {tab.index}
          </button>
        ))}
      </div>
      <TransitionGroup className="wrapper">
        <CSSTransition key={tabIndex} classNames="slide" timeout={1500}>
          <div className="main">{tabs[tabIndex].content}</div>
        </CSSTransition>
      </TransitionGroup>
    </Root>
  )
}
const Root = styled.div`
  .slide-enter {
    transform: translateX(100%);
  }
  .slide-enter-active {
    transform: translateX(0%);
    transition: transform 1500ms ease-in-out;
  }
  .slide-exit {
    transform: translateX(0%);
  }
  .slide-exit-active {
    transform: translateX(-100%);
    transition: transform 1500ms ease-in-out;
  }
  padding: 5px;
  height: 100vh;
  display: flex;
  flex-direction: column;
  .tabs {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
  }
  .wrapper {
    position: relative;
    border: slid 1px #444;
    flex: 1;
  }
  .main {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 10px;
  }
`
export default page

4. SwitchTransition

SwitchTransition は、TransitionGroup のコンポーネントである。公式には、Vue.js の transition modesインスパイヤされたコンポーネントと書いてある。SwitchTransition には、以下の2つのモードがある。

mode 説明
out-in 古いコンポーネントが削除されるまで待機してから、新しいコンポーネントを挿入する。
in-out 最初に新しいコンポーネントを挿入し、挿入後に、古いコンポーネントを削除する。

「Switch」 と書いてあるように、用途は限定的である。新しいコンポーネントと古いコンポーネントに対して、同時にアニメーションを実行できないので、同時に実行したい場合は、TransitionGroup を使うべきである。

上記のタブ切り替えを SwitchTransition に変えた場合、滑らかというよりは、ワンテンポ遅れている感じがして少し違和感がある。用途に応じて使うべきであると思う。

f:id:serip39:20201002152614g:plain

まとめ

今回は、ReactでCSSアニメーションをどのように実装すべきかを整理した。react-transition-group は非常に使いやすいと思えた。

それでは、ステキな開発ライフを。

関連リンク

ja.reactjs.org