▶️

[React] React と Anime.js でアニメーションを実装する

に公開

1. Anime.js と React

Anime.js はアニメーションを作成するための軽量な JavaScript ライブラリです。

オブジェクトの移動・変形・回転など単純な命令を組み合わせて、Web 画面上で複雑なアニメーションを実現することができます。

一方で React は Web サイトや Web アプリケーションの UI(ユーザーインターフェース)を構築するための JavaScript ライブラリで、SPA(シングルページアプリケーション)の作成などに活用できます。

以前 React で作成した Web 画面 で Anime.js でアニメーションを表現しようとしたところ何点かつまずいた点があったため、同じところで詰まってしまった人のために解決法を残しておこうと思います。

環境

  • React: バージョン 19.1.0
  • Anime.js:バージョン 4.0.2

2. createTimeline() を呼び出す場所

悪い実装例

以下のような、「アニメーションを再生」ボタンを押すとアニメーションが再生されるシンプルな画面の実装を想定します。

私は当初 React で以下のように実装しました。

❌ 悪い実装例

import { useEffect } from "react";
import { createTimeline } from "animejs";
import "./App.css";

function App() {
  const timeline = createTimeline({ autoplay: false });

  useEffect(() => {
    timeline
      .add(".square", {
        x: 240,
        duration: 1000,
      })
      .add(".square", {
        rotate: "1turn",
        duration: 1000,
      });
  }, []);

  return (
    <>
      <div className="animation-area">
        <div className="square" />
      </div>
      <button
        onClick={() => {
          timeline.init().play();
        }}
      >
        アニメーションを開始
      </button>
    </>
  );
}

export default App;

(CSS ファイルは省略します)

マウント時に起こることを順番に記すと以下のようになります。

createTimeline()により timeline が作成される。

② クラス名squareの要素などがレンダリングされる。

useEffect(() => { 処理 }, [])内の処理により、timeline にクラス名squareの要素を移動・回転させるアニメーションが追加される。

timeline をuseEffect外で作成しているのは、useEffect外でアニメーションの再生メソッドを呼び出せるようにするためです。

そして実際に「アニメーションを再生」ボタンを押下するとtimeline.init().play()が呼び出され、先ほどの画像のようなアニメーションが再生されます。

一見差し支えなく動いているように見えますが、この実装にはstate の値が変更されるなど画面の再レンダリングが起こるだけでアニメーションが再生されなくなるという大きな問題があります。

...

function App() {
+ const [count, setCount] = useState(0);

...

      </button>
+     <button onClick={() => setCount((prev) => prev + 1)}>
+       counts: {count}
+     </button>

...

原因は state 値の更新によってコンポーネントが更新され、①createTimeline()がもう一度呼び出されることで新たな timeline が作成され、timeline.init().play()を呼び出してもアニメーションが再生されなくなるためです。

正しい実装例

Anime.js を React とともに使う方法については公式のドキュメントに記載があります。

Anime.js can be used with React by combining React's useEffect() and Anime.js createScope() methods.

例示されているコードはanimate()を使用したものですが、createTimeline()を使用する場合も例と同様に useEffect() 内で呼び出すことになります。

⭕️ 正しい実装例

import { useEffect, useRef, useState } from "react";
import { createScope, createTimeline, Scope } from "animejs";
import "./App.css";

function App() {
  const [count, setCount] = useState(0);

  const root = useRef(null);
  const scope = useRef<Scope>(null);

  useEffect(() => {
    scope.current = createScope({ root }).add((self) => {
      const timeline = createTimeline({ autoplay: false });
      timeline
        .add(".square", {
          x: 240,
          duration: 1000,
        })
        .add(".square", {
          rotate: "1turn",
          duration: 1000,
        });

      self.add("playTimeline", () => {
        timeline.init().play();
      });
    });
    return () => scope.current?.revert();
  }, []);

  return (
    <>
      <div className="animation-area" ref={root}>
        <div className="square" />
      </div>
      <button
        onClick={() => {
          scope.current?.methods.playTimeline();
        }}
      >
        アニメーションを再生
      </button>
      <button onClick={() => setCount((prev) => prev + 1)}>
        counts: {count}
      </button>
    </>
  );
}

export default App;

マウント時に起こることを順番に記すと以下の通りです。

① クラス名squareの要素などがレンダリングされる。

useEffect(() => { 処理 }, [])内の処理により、createTimeline()で timeline が作成され、timeline にクラス名squareの要素を移動・回転させるアニメーションが追加される。

createTimeline()はマウント時の初回レンダリング後に一度だけ呼び出されます。

これにより再レンダリングが起こった際に再びcreateTimeline()で timeline が作成されることはなくなり、アニメーションの再生を行うことができるようになります。

なお useEffect 外でアニメーションの再生メソッドを呼び出すために Scope methods を利用しています。

3. Targets の指定

animate()timeline.add()の第1引数Targetsには、'.square'のような CSS セレクタ以外にも DOM 要素を指定することもできます。

React で DOM 要素を取得するためにはuseRef フックを使用します。

先ほどの実装を DOM 要素で指定するよう修正すると以下のようになります。

import { useEffect, useRef, useState } from "react";
import { createScope, createTimeline, Scope } from "animejs";
import "./App.css";

function App() {
  const [count, setCount] = useState(0);

  const root = useRef(null);
  const scope = useRef<Scope>(null);
+ const squareRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    scope.current = createScope({ root }).add((self) => {
+     if (squareRef.current) {
        const timeline = createTimeline({ autoplay: false });
        timeline
-         .add(".square", {
+         .add(squareRef.current, {
            x: 240,
            duration: 1000,
          })
-         .add(".square", {
+         .add(squareRef.current, {
            rotate: "1turn",
            duration: 1000,
          });

        self.add("playTimeline", () => {
          timeline.init().play();
        });
+     }
    });
    return () => scope.current?.revert();
  }, []);

  return (
    <>
      <div className="animation-area" ref={root}>
-       <div className="square" />
+       <div className="square" ref={squareRef} />
      </div>
      <button
        onClick={() => {
          scope.current?.methods.playTimeline();
        }}
      >
        アニメーションを再生
      </button>
      <button onClick={() => setCount((prev) => prev + 1)}>
        counts: {count}
      </button>
    </>
  );
}

export default App;

ここで注意する点としてTargetsは null 非許容のため、参照を呼び出す前にif (squareRef.current)で null でないことを確定させておく必要があります。

4. 最後に

Anime.js の公式ドキュメントは実際のアニメーションも交えながらわかりやすく解説されているため、一度目を通しておくことをお勧めします。

ただ React 関連の記述はUsing with Reactの 1 ページのみなので、React と併用する場合は実際に触りながら実装方法を探っていく必要がありそうです。

株式会社ブレイクエッジ 技術ブログ

Discussion