React学習 React学習ログ

【React学習ログ⑦】react-transition-groupでアニメーションを実装してみた

Reactにはアニメーションを実装する方法はいくつかあります。

本記事では「react-transition-group」と言うパッケージを使ったアニメーションの実装を行います。

react-transition-groupで代表的な機能は3つです。

  • Transition:基本型
  • CSSTransition:クラス自動付与
  • TransitionGroupリストのアニメーション化

それぞれ解説していきます。

状態を条件分けできるTransition

まずは「react-transition-group」をインストールします。

npm install react-transition-group

インストールしたものを読み込みます。

さらにTransitionコンポーネントでアニメーションさせたい要素を囲みます。

import Transition from 'react-transition-group/Transition';

const App = () => {
  return (
    <div className="container">
      <h1>アニメーション</h1>
      <div className="btn-container">
        <div className="btn btn-on" onClick={onHandler}>
          ON
        </div>
        <div className="btn btn-off" onClick={offHandler}>
          OFF
        </div>
      </div>
      <Transition in={onOff} timeout={500}>
        //アニメーションさせたい要素
      </Transition>
    </div>
  );
};

export default App;

Transitionコンポーネントは2つのプロパティを必須で持ちます。

  • in:真偽値(true/false)を渡すことができ、アニメーションのトリガーを制御するために使用
  • timeout:アニメーションの時間をミリ秒で設定するために使用

このように記述します↓

<Transition in={true/false} timeout={500}></Transition>

さらにTransitionコンポーネントではアニメーションの状態を管理するために「state」を使用します。

「state」について具体例を見てみましょう。

const App = () => {
  const [onOff, setOnOff] = useState(false);

  const onHandler = () => {
    setOnOff(true);
  };
  const offHandler = () => {
    setOnOff(false);
  };

  return (
    <div className="container">
      <h1>アニメーション</h1>
      <div className="btn-container">
        <div className="btn btn-on" onClick={onHandler}>
          ON
        </div>
        <div className="btn btn-off" onClick={offHandler}>
          OFF
        </div>
      </div>
      <Transition in={onOff} timeout={500}>
        {(state) => <p>{state}</p>}
      </Transition>
    </div>
  );
};

この実装は「ON」を押すとinプロパティの中身がtrueになり、「OFF」を押すとfalseになります。

そして、{state}にstateの状態のテキストが表示されます。

stateはentering→entered、exiting→exitedの流れで状態が変化していきます。

  • entering:{in=true}でアニメーションが実行されたとき
  • entered:{in=true}でアニメーションが完了した時
  • exiting:{in=false}でアニメーションが実行されたとき
  • exited:{in=false}でアニメーションが完了した時

では実際に動きを確認してみましょう。

まず最初「exited」が表示されています。

これはuseStateでin={onOff}の初期値がfalseとなっているためです。

ONボタンを押すとin={true}となりテキストがentering→entered、OFFボタンを押すとin={false}となりexiting→exitedにそれぞれ500ミリ秒の間隔で変化していきます。

この状態の変化に合わせてCSSを付与したり削除したりしてアニメーションを実現します。

次の実装でONボタンを押すと黄色ボックスがフェードイして、その後OFFボタンを押すと黄色ボックスがフェードアウトします。

const App = () => {
  const [onOff, setOnOff] = useState(false);

  const onHandler = () => {
    setOnOff(true);
  };
  const offHandler = () => {
    setOnOff(false);
  };

  return (
    <div className="container">
      <h1>アニメーション</h1>
      <div className="btn-container">
        <div className="btn btn-on" onClick={onHandler}>
          ON
        </div>
        <div className="btn btn-off" onClick={offHandler}>
          OFF
        </div>
      </div>
      <Transition in={onOff} timeout={500}>
        {(state) => (
          <div
            style={{
              width: '100px',
              height: '100px',
              margin: 'auto',
              backgroundColor: '#f1c70e',
              transition: 'opacity 0.3s ease-in',
              opacity: state === 'entered' ? 1 : 0,
            }}
          ></div>
        )}
      </Transition>
    </div>
  );
};

この実装で重要なポイントは「opacity: state === 'entered' ? 1 : 0」です。この部分で条件分けしています。ここは「entered」でないとうまく動きません。

「entered」ではなく他の状態を入力するとどうなるでしょう?

「entering」だとONボタンを押すとフェードイン後すぐに「entered」に変わり、フェードアウトします。

「exiting」だとONボタンを押しても何も変化せず、OFFボタンを押すとフェードイン後すぐに「exited」に変わりフェードアウトします。

「exited」だとOFFボタンでフェードインしそのまま表示され続けます。

ちなみにCSSを別で管理して、クラス名を動的につけることも可能です。

.anim-box {
  width: 100px;
  height: 100px;
  margin: auto;
  opacity: 0;
  background-color: #f1c70e;
  transition: opacity 0.3s ease-in;
}

.anim-box.fadein {
  opacity: 1;
}
<Transition in={onOff} timeout={500}>
  {(state) => {
    const cssClass = state === 'entered' ? ['anim-box', 'fadein'] : ['anim-box'];
    return <div className={cssClass.join(' ')}></div>;
  }}
  )}
</Transition>

以上がTransitionの基本的な使い方です。

ここからはTransition、CSSTransition、TransitionGroupで共通する機能を紹介します。

さて、先ほどの実装例では今の状態だとopacity:0の状態になって見えていないだけで、DOM上に残っています。

フェードイン前と、フェードアウト後に要素自体がDOM上から消えて欲しい状況の方が多いので次はその方法を解説します。

mountOnEnterとunmountOnExit

「フェードイン前と、フェードアウト後に要素自体がDOM上から消えて欲しい」と言うのはつまり、「entering前とexited後にDOMから消えて欲しい」と言うことです。

そこでTransitionコンポーネントに次の2つのプロパティを設定します。

  • mountOnEnter:entering前にはDOMに描画されない
  • unmountOnExit:exited後にDOMから消される

では例を見ていきます。

まずはmountOnEnterから、

<Transition in={onOff} timeout={500} mountOnEnter>
  {(state) => (
    <div
      style={{
        width: '100px',
        height: '100px',
        margin: 'auto',
        backgroundColor: '#f1c70e',
        transition: 'opacity 0.3s ease-in',
        opacity: state === 'entered' ? 1 : 0,
      }}
    ></div>
  )}
</Transition>

このようにONボタンを押してから右の検証モードの画面にdivが追加されたのがわかります。
しかしOFFボタンをを押しても追加されたdivは消えません。

ではunmountOnExitもつけてみましょう

<Transition in={onOff} timeout={500} mountOnEnter unmountOnExit>
  {(state) => (
    <div
      style={{
        width: '100px',
        height: '100px',
        margin: 'auto',
        backgroundColor: '#f1c70e',
        transition: 'opacity 0.3s ease-in',
        opacity: state === 'entered' ? 1 : 0,
      }}
    ></div>
  )}
</Transition>

unmountOnExitを追加することで、OFFボタンを押した後にがDOMから消されました。

timeoutプロパティはオブジェクト形式で管理できる

少し話は戻りますが、Transitionコンポーネントのtimeoutプロパティはアニメーションの時間を設定するために使用されると説明しました。

timeoutプロパティですが最初の例では数値を入れていましたが、実はオブジェクトを渡すこともできます。

オブジェクトで渡す場合はenteringとexitingの2つのアニメーション時間を別々に設定することが可能です。

const timeout = {
    enter: 1000,
    exit: 2000,
};

<Transition in={props.show} timeout={timeout} mountOnEnter unmountOnExit>
      {(state) => (
        // 省略

このように記述するとenteringで1秒、exitingで2秒をかけてアニメーションさせることができます。

別コンポーネントに記述する

もちろん別のコンポーネントにTransitionコンポーネントを作って読み込ませることも可能です。

今までApp.jsに書いていたアニメーションをAnim.jsと言うファイルに移して、コンポーネントとして読み込むようにしましょう。

コンポーネントの親子関係

実際の実装ではこのパターンの方が多くなると思います。

まずはApp.js。

import { useState } from 'react';
import Anim from './components/Anim';

const App = () => {
  const [onOff, setOnOff] = useState(false);

  const onHandler = () => {
    setOnOff(true);
  };
  const offHandler = () => {
    setOnOff(false);
  };

  return (
    <div className="container">
      <h1>アニメーション</h1>
      <div className="btn-container">
        <div className="btn btn-on" onClick={onHandler}>
          ON
        </div>
        <div className="btn btn-off" onClick={offHandler}>
          OFF
        </div>
      </div>
      <Anim show={onOff} />
    </div>
  );
};

export default App;

show={onOff}でAnim.jsにtrue/falseの値を渡しています。

続いてAnim.js。

import Transition from 'react-transition-group/Transition';

const Anim = (props) => {
  const timeout = {
    enter: 1000,
    exit: 2000,
  };
  return (
    <Transition in={props.show} timeout={timeout} mountOnEnter unmountOnExit>
      {(state) => (
        <div
          style={{
            width: '100px',
            height: '100px',
            margin: 'auto',
            backgroundColor: '#f1c70e',
            transition: 'opacity 0.3s ease-in',
            opacity: state === 'entered' ? 1 : 0,
          }}
        >
        </div>
      )}
    </Transition>
  );
};

export default Anim;

Transitionコンポーネントのin={props.show}でApp.jsからの値(true/false)を取得しています。

イベントを設定する

Transitionコンポーネントにイベントを設定することが可能です。

イベントは6つのタイミングで設定することができます。

  1. onEnter:inプロパティがtrueになるONボタンを押した時
  2. onEntering:①のアニメーション中
  3. onEntered:②のアニメーションの完了時
  4. onExit:inプロパティがfalseになるOFFボタンを押した時
  5. onExiting:④のアニメーション中
  6. onExited:⑤のアニメーションの完了時
const Anim = (props) => {
  const timeout = {
    enter: 1000,
    exit: 2000,
  };
  return (
		<Transition
		    in={this.state.showBlock}
		    timeout={timeout}
		    mountOnEnter
		    unmountOnExit
		    onEnter={() => console.log('onEnter')}
		    onEntering={() => console.log('onEntering')}
		    onEntered={() => console.log('onEntered')}
		    onExit={() => console.log('onExit')}
		    onExiting={() => console.log('onExiting')}
		    onExited={() => console.log('onExited')}
		  >
		    {(state) => (
		      //省略

実装結果はこちら。

以上がTransitionコンポーネントを用いたアニメーションの実装です。

以上がTrasition、CSSTransition、TransitionGroup共通で使える機能です。

自動でクラスを作るCSSTransition

続いて「react-transition-group」のCSSTransitionについて解説します。

結論から言うと、任意のクラス名を設定するとそのクラス名に紐付いた6つのクラスを自動で作成してくれ、アニメーションの状態に合わせてクラス名を自動で変更してくれます。

まずは実装方法を見てみましょう。

import { useState } from 'react';
import CSSTransition from 'react-transition-group/CSSTransition';

const App = () => {
  const [onOff, setOnOff] = useState(false);

  const onHandler = () => {
    setOnOff(true);
  };
  const offHandler = () => {
    setOnOff(false);
  };

  return (
    <div className="container">
      <h1>アニメーション</h1>
      <div className="btn-container">
        <div className="btn btn-on" onClick={onHandler}>
          ON
        </div>
        <div className="btn btn-off" onClick={offHandler}>
          OFF
        </div>
      </div>
      <CSSTransition
        in={onOff}
        timeout={1000}
        mountOnEnter
        // unmountOnExit
        classNames="emaxple"
      >
        <div className="anim-box"></div>
      </CSSTransition>
    </div>
  );
};

export default App;

2行目でCSSTransitionをインポートしています。

アニメーションをさせたい要素をで囲みます。

Transitionコンポーネントとは違い「state」を使ってのアニメーションの状態管理ではなくなりました。

※説明のため今回はunmountOnExitをコメントアウトします。

classNamesプロパティが重要で、ここに任意のクラス名を入れます。
このクラス名に紐づくクラスが自動で生成されます。

今回「example」と設定しているので生成されるクラス名はこの6つです。

  • emaxple-enter:in={true}でのアニメーション開始時
  • emaxple-enter-active:in={true}でのアニメーション中
  • emaxple-enter-done:in={true}でのアニメーション終了時
  • emaxple-exit:in={false}でのアニメーション開始時
  • emaxple-exit-active:in={false}でのアニメーション中
  • emaxple-exit-done:in={false}でのアニメーション終了時

自動でクラスが生成させれるので、事前に生成されるクラスにそれぞれスタイルを設定します。

.anim-box {
  width: 100px;
  height: 100px;
  margin: auto;
  opacity: 1;
  background-color: #f1c70e;
  transition: all 0.3s ease-in;
}
//↓生成されるクラスに事前にスタイルを設定する
.emaxple-enter {
  width: 100px;
}
.emaxple-enter-active {
  width: 200px;
}
.emaxple-enter-done {
  width: 300px;
}
.emaxple-exit {
  width: 300px;
}
.emaxple-exit-active {
  width: 200px;
}
.emaxple-exit-done {
  width: 100px;
}

実装結果はこちら。

クラスが自動的に付与されていっていますね。

-enterと-enter-active、-exitと-exit-activeは同時に付与されます。

CSSTransitionでは何もしなければ自動で6つクラス名が付与されるのですが、これらのクラス名をそれぞれ自分で設定する方法もあります。

classNamesにオブジェクトでこのようにすれば自由にクラス名を設定できます。左側の値は固定で、右側にクラス名をつけます。

classNames={{
     enter: 'ENTRE',
     enterActive: 'ENTER-ACTIVE',
     enterDone: 'ENTER-DONE',
     exit: 'EXIT',
     exitActive: 'EXIT-ACTIVE',
     exitDone: '', //クラスが不要な時は「''」にする
    }}

リスト追加/削除時に使うTransitionGroup

次はリストに対してアニメーションを実装していきます。

こんな感じです。

まず、アニメーションなしでリストの追加・削除の実装を行います。

import { useState } from 'react';

const App = () => {
  const [listItems, setListItems] = useState([1, 2, 3]);

  const onHandler = () => {
    setListItems((prevList) => prevList.concat(prevList.length + 1));
  };
  const offHandler = () => {
    setListItems((prevList) => {
      const newList = [...prevList];
      newList.pop();
      return newList;
    });
  };

  return (
    <div className="container">
      <h1>アニメーション</h1>
      <div className="btn-container">
        <div className="btn btn-on" onClick={onHandler}>
          Add
        </div>
        <div className="btn btn-off" onClick={offHandler}>
          Remove
        </div>
      </div>
      <ul>
        {listItems.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

export default App;

結果はこうなります。(CSSの解説はしません)

リストをアニメーションさせるには「react-transition-group」のTransitionGroupを使います。

import { useState } from 'react';
import TransitionGroup from 'react-transition-group/TransitionGroup';
import CSSTransition from 'react-transition-group/CSSTransition';

const App = () => {
  //省略

  return (
    <div className="container">
       //省略
      <TransitionGroup component="ul">
        {listItems.map((item, index) => (
          <CSSTransition key={index} classNames="fade" timeout={500}>
            <li>{item}</li>
          </CSSTransition>
        ))}
      </TransitionGroup>
    </div>
  );
};

export default App;

2行目にTransitionGroupを読み込んでいます。

アニメーションさせたい連続した要素をTransitionGroupで囲みます。

今回は先ほど学んだCSSTransitionも一緒に使ってアニメーションを実装します。

TransitionGroupにはcomponentプロパティがあります。
このプロパティに要素名を入れ込むことでDOMにはその要素として描画されます。
今回の例では「component="ul"」としているのでDOM上ではとして存在することになります。

あとは配列をループさせて値の数だけリストを生成するようにmap()を使って、各リストがアニメーションされるようにで囲めば完成です。

注意点は「key={index}」がliではなく、CSSTransitionにつけられている点です。
CSSTransitionでliを囲んでいるので、keyの設置場所が変わります。

参考【React学習ログ②】Keyについて調べまくって役割を言語化してみた

続きを見る

CSSTransitionで生成されるCSSの記述はこちらです。

.fade-enter {
  opacity: 0;
  transform: translateX(-50px);
}
.fade-enter-active {
  opacity: 1;
  transform: translateX(0px);
  transition: all 0.3s ease-in;
}
.fade-exit {
  opacity: 1;
}
.fade-exit-active {
  transform: translateX(50px);
  opacity: 0;
  transition: all 0.3s ease-in;
}

これで完成です。

以上がReactでのアニメーションの実装方法の解説です。

本記事では「react-transition-group」を使って実装しましたが、他にもの方法はあるようです。

ぜひ実装しやすい方法を見つけて積極的に使っていきましょう。

最後に

このコンテンツでは一般のエンジニアがどのように言語(今回はフレームワークになりますが)を学習し、つまずき、乗り越えていっているのかを垣間見えるようにしていこうと思います。

できるだけ画像、動画などを盛り込んで視覚的に理解できるように工夫しています。

少しでもこれからプログラミングを学ぼうと考えている方の為になる発信をできれば思いますので、温かい目で見てもらえると嬉しいです。

今回私が解説する上で使った教材は、Udemyのコースになります。興味があればぜひ使ってみてください。

このコースの補足教材としてこのブログを使ってもらえると効果的だと思います。

ベストセラー取得

-React学習, React学習ログ