React で Fade in, out を実装する

Note

#React

#TypeScript

syakoo2021-06-01 更新)

React で Fade in, out のアニメーションの実装にハマったので、残していきます

visibility を用いたカスタムフックを使う方法と、レンダリングを制御したコンポーネントによる実装を紹介します

visibility で実装

一つ目は、CSS の visibility を用いてカスタムフックで実装します。 visibility: hidden になると表示されなくなるため、この時は opacity のアニメーションを待つようにします

import { CSSProperties, useCallback, useMemo, useState } from "react";
 
const useFadeInOut = (durationSec: number) => {
  const [display, setDisplay] = useState(false);
 
  const handleClose = useCallback(() => {
    setDisplay(false);
  }, [setDisplay]);
 
  const handleOpen = useCallback(() => {
    setDisplay(true);
  }, [setDisplay]);
 
  const toggleDisplay = useCallback(() => {
    setDisplay((prev) => !prev);
  }, [setDisplay]);
 
  const boxStyle = useMemo((): CSSProperties => {
    if (display) {
      return {
        opacity: 1,
        visibility: "visible",
        transition: `opacity ${durationSec}s`,
      };
    }
 
    return {
      opacity: 0,
      visibility: "hidden",
      transition: `opacity ${durationSec}s, visibility 0s ${durationSec}s`,
    };
  }, [durationSec, display]);
 
  return { display, handleOpen, handleClose, toggleDisplay, boxStyle };
};

あとは次のように使います:

const App: React.VFC = () => {
  const { toggleDisplay, boxStyle } = useFadeInOut(0.2);
 
  return (
    <>
      <button onClick={toggleDisplay}>Button</button>
      <div style={boxStyle}>
        <Modal />
      </div>
    </>
  );
};

デモ (CodeSandbox)

レンダリングを制御

表示しないときはレンダリングしないようにします。 次のように結構複雑になります:

import React, { useEffect, useMemo, useRef, useState } from "react";
 
type FadeInOutBoxProps = {
  display: boolean;
};
 
type DisplayState = "DISPLAY" | "HIDDEN";
 
const FadeInOutBox: React.FC<FadeInOutBoxProps> = ({ children, display }) => {
  const [displayState, setDisplayState] = useState<DisplayState>("HIDDEN");
  const boxRef = useRef<HTMLDivElement>(null);
 
  const style = useMemo((): React.CSSProperties => {
    if (!display || displayState === "HIDDEN") {
      return {
        opacity: 0,
        transition: "0.2s",
      };
    }
 
    return {
      opacity: 1,
      transition: "0.2s",
    };
  }, [display, displayState]);
 
  useEffect(() => {
    if (display && displayState === "HIDDEN") {
      setDisplayState("DISPLAY");
    }
    const onEvent = () => {
      if (!display && displayState === "DISPLAY") {
        setDisplayState("HIDDEN");
      }
    };
    const box = boxRef.current;
 
    box?.addEventListener("transitionend", onEvent);
    return () => {
      box?.removeEventListener("transitionend", onEvent);
    };
  }, [display, displayState, boxRef]);
 
  return (
    <>
      {(display || displayState === "DISPLAY") && (
        <div
          ref={boxRef}
          style={{
            background: "red",
            ...style,
          }}
        >
          <div>{children}</div>
        </div>
      )}
    </>
  );
};

簡単に解説すると、表示の状態である displayState を定義して次の 4 つの状態を持つようにします:

| display | displayState | 状態 | | :-------: | :------------: | :--------: | | True | "HIDDEN" | Fade in | | True | "DISPLAY" | 描写済み | | False | "DISPLAY" | Fade out | | False | "HIDDEN" | 描写しない |

描写しないのは display = false, displayState = "HIDDEN" の時のみで、それ以外は描写はするようにします。 また、描写はじめと描写おわりの時は opacity0 にする必要があります。 さいごに、useEffect を使って、display = True になったときは displayState = "DISPLAY" に、 display = False でアニメーションが終わったら displayState = "HIDDEN" にします。

デモ (CodeSandbox)