🎃

【React.js】animate()メソッドを使ってコンポーネントのmount/unmount時にfade-in/fade-outさせる

2024/08/18に公開

はじめに

fade-in/fade-outは主にモーダル・ダイアログでよく使用されています。
fade-inはCSSのtransitionやanimationを用いることで簡単に実装できます。
fade-outはReactだと不透明度を0にして視覚上見えなくしたり[1]、framer-motionなどの外部ライブラリを頼ることが多いと思います。

今回はブラウザ標準の機能であるElement.animate()メソッドを使ってmount/unmountに連動してfade-in/fade-outを行うモーダルを実装してみます。

前提知識

  • HTML
  • CSS
  • JavaScript
  • TypeScript
  • React.js

インターフェイス

型は以下の通りです。

export type ModalProps = {
  isOpen: boolean;
  onCancell: () => void;
  children?: ReactNode;
};

以下のように使用されることを想定します。

const App = () => {
  const [isOpen, setIsOpen] = useState(false)
  return (<>
    <div>
      <button onClick={() => { setIsOpen(true) }}>モーダルを開く</button>
    </div>
    <Modal isOpen={isOpen} onCancell={() => { setIsOpen(false) }} >
      <Hoge />
    </Modal>
  </>
  )
}

ソースコード

以下が実装したコンポーネントのソースコードです(機能以外の部分は必要最低限です)。

export const Modal = ({ isOpen, onCancell, children }: ModalProps) => {
  const modalRef = useRef<HTMLDivElement>(null);

  // fade-out
  const startFadeoutModalAnimation = async () => {
    return modalRef.current?.animate([{ opacity: 1 }, { opacity: 0 }], {
      duration: 300,
      fill: "forwards",
    }).finished;
  };

  // mount時にfade-inする
  useEffect(() => {
    if (!modalRef.current) {
      return;
    }
    if (!isOpen) {
      return;
    }

    modalRef.current?.animate([{ opacity: 0 }, { opacity: 1 }], {
      duration: 300,
      fill: "forwards",
    });
  }, [isOpen]);

  if (!isOpen) {
    return;
  }

  return (
    <div
      ref={modalRef}
      style={{
        position: "absolute",
        top: 0,
        left: 0,
        zIndex: 100,
        background: "rgb(0, 0, 0, 0.6)",
        width: "100vw",
        height: "100vh",
        display: "grid",
        placeContent: "center",
        gridTemplateColumns: "minmax(auto, 88%)",
        gridTemplateRows: "minmax(auto, 88%)",
      }}
    >
      <div
        style={{
          backgroundColor: "#fff",
        }}
      >
        <div
          style={{
            display: "flex",
            flexDirection: "row",
            justifyContent: "space-between",
            borderRadius: "1rem",
            padding: 12,
          }}
        >
          <div>This is Modal</div>
          <button
            style={{
              alignSelf: "flex-start",
              height: 32,
              width: 32,
              borderRadius: "50%",
              backgroundColor: "#efefef",
              cursor: "pointer",
            }}
            onClick={() => {
              startFadeoutModalAnimation().then(() => {
                onCancell();
              });
            }}
          >
            ×
          </button>
        </div>
        <div>{children}</div>
      </div>
    </div>
  );
};

画面上に表示したものがこちらです。

fade-inの実装

CSSだけでも実装できますが、今回はあえてanimate()メソッドを使うためにuseEffectで実装します。

  useEffect(() => {
    if (!modalRef.current) {
      return;
    }
    if (!isOpen) {
      return;
    }

    modalRef.current?.animate([{ opacity: 0 }, { opacity: 1 }], {
      duration: 300,
      fill: "forwards",
    });
  }, [isOpen]);

fade-outの実装

失敗例:useEffectのクリーンアップ関数でfade-outを行う

こちらのコードは失敗例です。useEffectのクリーンアップ関数は依存配列の変数が変化したとき及びコンポーネントがunmountされるときに実行されるのですが、実行時には既に画面から消えてしまっているため、fade-outさせることができません

  useEffect(() => {
    // fade-inの処理
    // ........

    return () => {
      modalRef.current?.animate([{ opacity: 1 }, { opacity: 0 }], {
        duration: 300,
        fill: "forwards",
      });
    }
  }, [isOpen]);

閉じるボタンのonClickイベントでfade-outを行う

Element.animate()メソッドは返り値としてAnimateオブジェクトを返します。このオブジェクトはアニメーションが終わったかどうかをpromiseで返すfinishedプロパティを持っています。
finishedのpromiseの解決を待ってからonCancellを呼び出すことでfade-outが終わってからunmountさせることができます。

以下がfade-out処理と閉じるボタンのonClickイベントです。

  // fade-out
  const startFadeoutModalAnimation = async () => {
    return modalRef.current?.animate([{ opacity: 1 }, { opacity: 0 }], {
      duration: 300,
      fill: "forwards",
    }).finished;
  };
  // ......
  return (
    // ......
    <button
      // ......
      onClick={() => {
        startFadeoutModalAnimation().then(() => {
        onCancell();
      });
      // ......
    </button>

最後に

Element.animation()メソッドはアニメーションをpromiseで扱えるため応用幅が広そうです。
作成したモーダルはサイズが固定ですが、サイズを自由に設定できたり子要素のサイズに合わせるようにすれば確認ダイアログにも使えそうです。

脚注
  1. この方法だと非表示のときもコンポーネントがmountされてしまうので要件次第では避けた方がよいです ↩︎

Discussion