use-animate-presenceを使ってReactのマウントとアンマウントのアニメーションを簡単に作る


TL;DR

use-animate-presenceをインストールしてデモの通りにすれば、すぐに実装できます。

レンダーとアニメーションのライフサイクルを同期させる

Reactのマウントとアンマウントのアニメーションでポイントとなるのは、レンダーとアニメーションのライフサイクルの同期。それは、ライブラリを使わなくても簡単にできることだけど、目的が遊び以外だったらほぼ確実にライブラリを使った方が無難だと思います。

簡単な実装からはじめる

Reactのマウントとアンマウントのアニメーションは昔からややこしいやり方を必要としていて、react-transition-groupのようなライブラリが生まれた。マウントなら、フェードインのようなアニメーションはJSを使わなくてもCSSだけで作れるが、アンマウントの場合ややこしくなります。

SvelteとかRiotにあるアンマウントの直前に実行されるライフサイクルメソッドがReactにはないのが原因です。例えば、普段は何かのbooleanでレンダーするかどうかを判断しています。それをfalseにすると、そのコンポーネントがすぐにReactのツリーから削除され、アニメーションをセットアップする時間がないです。削除を延長させたい場合は、トリックが必要です。

一番簡単なトリックは、もう一つをbooleanを定義して、trueになった場合に、CSSアニメーションをはじめるCSSのクラスを追加するというアプローチです。そして、アニメーションをさせたいエレメントに onAnimationEnd リスナーを付与する。それが実行されたら、今度はレンダーするかどうかのbooleanをfalseにすると、やっとアンマウントできます。

export default function App() {
  const [animateBeforeUnmount, setAnimateBeforeUnmount] = React.useState(false);
  const [isRendered, setIsRendered] = React.useState(true);
  const divRef = React.useRef();

  const handleAnimationEnd = () => {
    setIsRendered(false);
    setAnimateBeforeUnmount(false);
  };

  return (
    <>
      <button onClick={() => setAnimateBeforeUnmount(true)}>Toggle</button>
      {isRendered && (
        <div
          onAnimationEnd={handleAnimationEnd}
          ref={divRef}
          className={`bg-square ${animateBeforeUnmount && "fade-out"}`}
        />
      )}
    </>
  );

Codesanboxで編集:

use-animate-presence

以上の例は簡単と言えば簡単だが、現実世界のアプリはそこまで簡単ではない。ロジックを再利用する必要があったり、もっと複雑なアニメーションと複数のアニメーションの同期が必要になったりします。それも簡単にするためにuse-animate-presenceを作りました。このフックは1KBちょっとしかないので、深く考えずにインストールできます。

このフックは

  • メインスレッド外でスムーズに実行される
  • バネ(Spring)の物理を使ったアニメーションをする
  • マウントとアンマウントをチェーンできる(レンダーとアニメーションのライフサイクル同期)

他にもいろいろあります。

use-animate-presenceを使って何ができるか以下のデモを見てください. アニメーション中にボタンを押して、アニメーションがいきなり止まることなくスムーズにキャンセルされることもわかります。

Codesandboxで編集:

このデモではバネが使われています。それはCSSアニメーションでは不可能ですが、Web Animations APIを裏で使っているのでCSSアニメーションのように実行中のJavascriptには邪魔されずに60FPSのアニメーションが保証されています!