React で Fade in, out を実装する

syakoo posted on
(Updated at: )

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)