🏃

[React + Emotion]アニメーションが完了してからアンマウントさせる

2024/09/03に公開

はじめに

この記事ではライブラリを使用せず、アニメーションが完了した後にアンマウントする実装について紹介します。
アニメーションが完了した後にアンマウントすることで、以下のGIFのようにスライドアウトアニメーションが最後まで実行できる様になります。

この挙動を実現するため、何が問題となるのかを先に紹介します。

問題点

Reactでコンポーネントが表示される時、コンポーネントが非表示になる時にアニメーションをさせたい場合、コンポーネントの表示・非表示の状態をブール値で持つ以下のような実装が思いつきます。
以下の実装では、ボタンを押下することでスライドインアニメーションで吹き出しが現れ、吹き出しが表示されている状態で再度ボタンを押下するとスライドアウトアニメーションで吹き出しが非表示になる事を期待しています。
(ReactとEmotionを利用して記述しています。本題と関係のない記述は省いています。)

export const Hoge = () => {
  const [isBalloonVisible, setBalloonVisibility] = useState<boolean>(false)

  return (
    <Container>
      <Button onClick={() => setBalloonVisibility((prev) => !prev)}>
        ボタン
      </Button>
      {isBalloonVisible && (
        <BalloonContainer isBalloonVisible={isBalloonVisible}>
          <Balloon>テキストテキストテキスト</Balloon>
        </BalloonContainer>
      )}
    </Container>
  )
}

const BalloonContainer = styled.div<{ isBalloonVisible: boolean }>`
  animation: ${({ isBalloonVisible }) =>
      isBalloonVisible ? slideIn : slideOut}
    300ms forwards;
`

こちらのコンポーネントのアニメーションは以下のGIFの様になります。
「はじめに」のGIFではスライドアウトアニメーションで吹き出しが消えましたが、以下のGIFでは吹き出しが一瞬で消えてしまっています。

これはスライドアウトアニメーションが完了する前に吹き出しコンポーネントがアンマウントされているためです。
次にコンポーネントが非表示になる時のアニメーションの実行が完了してからコンポーネントをアンマウントする方法を記述します。

解決策

react-transition-groupというライブラリを利用すると上記問題を解決できるようですが[1]、2024年9月現在、こちらのライブラリの最終リリースが2022年8月1日となっていること[2]、あまりプロジェクトにライブラリを追加したくないなどの理由でライブラリを使わずに実装したので、その方法を記述します。
実装の要点は以下です。

  • コンポーネントの状態をvisiblehidinghiddenの3つで持つ
  • コンポーネントがhidden状態以外の場合、コンポーネントをマウントさせる
  • コンポーネントがvisible状態の際、再度ボタンを押下することで、コンポーネントをhiding状態にする
    • コンポーネントがhiding状態の際、コンポーネントが非表示になる時のアニメーションを実行させる
  • onAnimationEndでコンポーネントの状態をhiddenにする(アンマウントさせる)
const BALLOON_STATUS = {
  visible: 'visible',
  hiding: 'hiding',
  hidden: 'hidden',
} as const
type BalloonStatus = (typeof BALLOON_STATUS)[keyof typeof BALLOON_STATUS]

export const Hoge = () => {
  const [balloonStatus, setBalloonStatus] = useState<BalloonStatus>(
    BALLOON_STATUS.hidden
  )
  const changeBalloonStatus = () => {
    if (balloonStatus === BALLOON_STATUS.hidden) {
      setBalloonStatus(BALLOON_STATUS.visible)
      return
    }
    if (balloonStatus === BALLOON_STATUS.visible) {
      setBalloonStatus(BALLOON_STATUS.hiding)
    }
  }
  const hideBalloon = () => {
    if (balloonStatus === BALLOON_STATUS.hiding) {
      setBalloonStatus(BALLOON_STATUS.hidden)
    }
  }

  return (
    <Container>
      <Button onClick={changeBalloonStatus}>ボタン</Button>
      {balloonStatus !== BALLOON_STATUS.hidden && (
        <BalloonContainer
          balloonStatus={balloonStatus}
          onAnimationEnd={hideBalloon}
        >
          <Balloon>テキストテキストテキスト</Balloon>
        </BalloonContainer>
      )}
    </Container>
  )
}

const BalloonContainer = styled.div<{ balloonStatus: BalloonStatus }>`
  ${({ balloonStatus }) => {
    switch (balloonStatus) {
      case BALLOON_STATUS.visible:
        return css`
          animation: ${slideIn} 300ms forwards;
        `
      case BALLOON_STATUS.hiding:
        return css`
          animation: ${slideOut} 300ms forwards;
        `
    }
  }};
`

こちらのコンポーネントのアニメーションは以下のGIFの様になります。
コンポーネントが非表示になる時のアニメーションの実行が完了した後、コンポーネントがアンマウントされるようになります。

まとめ

  • Reactでアニメーションの完了を待ってからアンマウントさせるにはひと手間必要
    • アニメーションさせるコンポーネントの状態をvisiblehidinghiddenで持つ
    • onAnimationEndでコンポーネントをアンマウントさせる
  • 特に理由がなければreact-transition-groupを導入しても良さそう
脚注
  1. Reactでアンマウント時のアニメーションをつけるのにReact Transition Groupを使ったら簡単だった ↩︎

  2. react-transition-groupのReleases ↩︎

株式会社ZOZO

Discussion