React で Fade in, out を実装する
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"
の時のみで、それ以外は描写はするようにします。
また、描写はじめと描写おわりの時は opacity
を 0
にする必要があります。
さいごに、useEffect
を使って、display = True
になったときは displayState = "DISPLAY"
に、
display = False
でアニメーションが終わったら displayState = "HIDDEN"
にします。