react-transition-groupを簡単に使ってみる


はじめに

reactでアニメーションをさせたいときの方法の一つとしてreact-transition-groupがあります。このパッケージのTransitionというコンポーネントを簡単に使ってみたのでわかったことなどまとめていきます。
この記事では以下の二つのことについて書きます。

  1. Transitionコンポーネントの使い方
  2. 親要素のWidthをアニメーション中に子要素が崩れて見えてしまうことに対して自分が行った対策

Transitionコンポーネントの使い方

ボタンを押したら何らかの設定画面が横から出てくる表現がしたかったため、Transitionを使ってこんな感じのアニメーションを実装しました。

簡単なアニメーションの流れ
右下のボタンを押すと画面右から要素が出てくる。
もう一度ボタンを押すと要素が閉じる。

はい、簡単にしすぎましたが、シンプルイズベスト的なアニメーションですね。
コードもシンプルです。

import React from 'react';
import { Transition } from 'react-transition-group';
import InnerSlide from '../InnerSlide';

const defaultStyle = {
  transition: 'all 0.4s ease-in',
  position: 'absolute',
  right: '0px',
  bottom: '0px',
  width: '0px',
  height: '100%',
};

const transitionStyles = {
  entering: {
    width: '70%',
  },
  entered: {
    width: '70%',
  },
  exiting: {
    width: '0px',
  },
  exited: {
    width: '0px',
  },
};

const SettingSlide = ({ show }) => (
  <Transition in={show} timeout={400}>
    {(state) => (
      <div style={{ ...defaultStyle, ...transitionStyles[state] }}>
        <InnerSlide state={state} />
      </div>
    )}
  </Transition>
);

export default SettingSlide;

初期のスタイルとアニメーションの各タイミングにおけるスタイルを用意しておき、Transitionから提供されるアニメーションの状態によって用意しておいたスタイルを適用しています。Transitionは、propsのinがtrueかfalseかによってアニメーションの開始タイミングを検知します。この場合は、showというpropsによって開始タイミングを制御してます。(最初に示したGifで右下に見えるボタンによってshow(初期値false)を変化させてます。)また、widthの初期値は0で、最終的に画面の70%まで0.4秒かけてwidthを広げています。
提供される状態はコンテキストのstateに入っており、全部で四種類返ってきます。
どのタイミングで返ってくるかは以下の通りです。

  • 'entering'
    inに渡された値がtrueになったとき。

  • 'entered'
    アニメーション終了時。

  • 'exiting'
    inに渡された値がfalseになったとき。

  • 'exited'
    アニメーション終了時。

コンソールでstateを見ると若干呼ばれているタイミングが違いましたが、上記の理解で大丈夫だと思います。
今回実装したアニメーションに当てはめてみるとこんな感じです。
初期状態width0 -> showがtrueになり、stateがenteringなる -> 0.4sかけてwidth70% -> enteredになる。(この場合はスタイルは特に何も変化させていない)
showがfalseになる -> exitingなる -> width0% -> exitedなる

実装はこんな感じでやりましたが、ここであることに気が付きます。↓

親要素のWidthをアニメーション中に子要素が崩れてしまう

widthを変化させてるため、アニメーションしている間(例えばwidthが20pxとか狭いとき)、スタイルが崩れて見えてしまいます。こんな感じです。

おわかりいただけただろうか?
この問題に対して3秒くらいで思いついた対応(例えばoverflowをhiddenにする)がいくつかありましたが、最終的は「Transitionのstateによって子要素を表示する」という方法をとりました。

before

アニメーションしてる親要素の子要素
import React from 'react';
import { BsFillPlusCircleFill } from 'react-icons/bs';
import { IconContext } from 'react-icons';

import styles from './style.module.css';

const Prests = () => (
  <div className={styles.container}>
    <div className={styles.header}>
      <p className={styles.title}>
        プリセット
      </p>
      <IconContext.Provider value={{ color: 'rgb(116, 187, 150)', className: `${styles.icon}`, size: '3rem' }}>
        <div>
          <BsFillPlusCircleFill />
        </div>
      </IconContext.Provider>
    </div>
  </div>
);

export default Prests;

after

アニメーションしてる親要素の子要素
import React from 'react';
import { BsFillPlusCircleFill } from 'react-icons/bs';
import { IconContext } from 'react-icons';

import styles from './style.module.css';

const Prests = ({ state }) => {
  console.log(state);
  return (
    <div className={styles.container}>
      {
        (state === 'entered') ? (
          <div className={styles.header}>
            <p className={styles.title}>
              プリセット
            </p>
            <IconContext.Provider value={{ color: 'rgb(116, 187, 150)', className: `${styles.icon}`, size: '3rem' }}>
              <div>
                <BsFillPlusCircleFill />
              </div>
            </IconContext.Provider>
          </div>
        ) : (
          <div />
        )
      }
    </div>
  );
};

export default Prests;

stateがenteredの時(アニメーションが終了した時のみ)に子要素を表示するようにしています。
それっぽくするために、(state === 'entered')で変化するフラグを用意して、それによってさらにTransitionで子要素にopacityアニメーションをつけてもいいなと思いました。