7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Reactでアンマウント時のアニメーションをつけるのにReact Transition Groupを使ったら簡単だった

Posted at

これは何

  • Reactでライブラリに頼らずにunmount時のアニメーションをつけようと思うと意外と大変
  • React Transition Groupを使うと楽
  • 上記をまとめた記事です
  • なお、この記事の内容はこちらのリポジトリで公開しており、GitHub Pagesで実際の挙動を見ることもできます
  • 「フロントエンド強化月間 - 開発する上で知っておくべき知見を共有しよう」イベントへの投稿記事でもあります。

環境など

パッケージ バージョン
react 17.0.2
@emotion/react 11.4.0
react-transition-group 4.4.1

当初遭遇した事象、状況

  • Reactを使ってコードを書いている
  • ボタンをクリックしたら要素が出たり消えたりするアニメーションを作りたかった
  • 出現するときのアニメーションは問題ない
  • 消えるときに瞬間的に消えてしまう

書いていたコード

若干長いですが、途中で出る用と消える用のキーフレームを指定してアニメーションをさせようとしていました。

import { useState } from "react";
import "./App.css";
import { css, keyframes } from "@emotion/react";

function App() {
  const [open, setOpen] = useState(false);
  const toggleOpen = () => setOpen((currentState) => !currentState);
  return (
    <div className="App">
      <p css={title}>CSS Only</p>
      {open && (
        <div css={window} className={open ? "open" : "close"}>
          <img
            src="https://picsum.photos/360/240"
            alt=""
            css={image}
            width="360"
            height="240"
          />
        </div>
      )}
      <button css={button} onClick={toggleOpen}>
        {open ? "Close" : "Open"}
      </button>
    </div>
  );
}

const title = css`
  align-self: flex-end;
  color: #fff;
  font-size: 40px;
  font-weight: bold;
`;

const windowOpenKeyframes = keyframes`
  from {
    opacity: 0;
    transform: translateY(50%);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
`;

const windowCloseKeyframes = keyframes`
  from {
    opacity: 1;
    transform: translateY(0);
  }
  to {
    opacity: 0;
    transform: translateY(-20%);
  }
`;

const window = css`
  align-self: center;
  background-color: #fff;
  border-radius: 8px;
  color: #212121;
  font-size: 40px;
  justify-self: center;
  opacity: 0;
  padding: 20px;
  transform: translateY(0);
  transition-duration: 300ms;
  transition-property: opacity, transform;
  transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
  width: 400px;
  &.open {
    animation: ${windowOpenKeyframes} ease 300ms both;
  }
  &.close {
    animation: ${windowCloseKeyframes} ease 300ms both;
  }
`;

const image = css`
  background-color: #eee;
  border-radius: 4px;
  display: block;
`;

const button = css`
  align-self: flex-start;
  background-color: #61dafb;
  border: none;
  border-radius: 8px;
  color: #212121;
  cursor: pointer;
  font-size: 20px;
  font-weight: bold;
  justify-self: center;
  padding: 20px 40px;
  text-decoration: none;
  width: 200px;
`;

export default App;

原因

分かってしまえば当たり前の話なのですが、Reactで要素がアンマウントされるときは一瞬で消えてしまいます。
openのstateがfalseになったら、closeのクラスが付与されるより先に画面から消えてしまうため、アニメーションがおこるはずもありません。

React Transition Groupの利用

openのstate以外にもう1つstateを用意して、openがfalseになったらCSSのクラスを付与して、アニメーションが終わったらそのstateもfalseにして、両方falseになったら消える……
などと書けばライブラリを使わなくてもできるような気がしますが1、ライブラリを使えば割と簡単に実装できました。

React Transition Groupには<Transition><CSSTransition>があります。
<Transition>はコンポーネントの中で更にstateを渡さないといけないので、今回は<CSSTransition>を選びました。

import { useState } from "react";
import "./App.css";
import { css, keyframes } from "@emotion/react";
import { CSSTransition } from "react-transition-group";

function App() {
  const [reactTransitionGroupOpen, setReactTransitionGroupOpen] =
    useState(false);
  const toggleReactTransitionGroupOpen = () =>
    setReactTransitionGroupOpen((currentState) => !currentState);
  return (
    <div className="App">
      <p css={title}>React Transition Group</p>
      <CSSTransition
        in={reactTransitionGroupOpen}
        timeout={200}
        classNames="react-transition-group"
        unmountOnExit
      >
        <div css={reactTransitionGroupWindow}>
          <img
            src="https://picsum.photos/360/240"
            alt=""
            css={image}
            width="360"
            height="240"
          />
        </div>
      </CSSTransition>
      <button css={button} onClick={toggleReactTransitionGroupOpen}>
        {reactTransitionGroupOpen ? "Close" : "Open"}
      </button>
    </div>
  );
}

const title = css`
  align-self: flex-end;
  color: #fff;
  font-size: 40px;
  font-weight: bold;
`;

const reactTransitionGroupWindow = css`
  align-self: center;
  background-color: #fff;
  border-radius: 8px;
  color: #212121;
  font-size: 40px;
  justify-self: center;
  padding: 20px;
  width: 400px;
  &.react-transition-group-enter {
    opacity: 0;
    transform: translateY(50%);
  }
  &.react-transition-group-enter-active {
    opacity: 1;
    transform: translateY(0);
    transition-duration: 300ms;
    transition-property: opacity, transform;
    transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
  }
  &.react-transition-group-exit {
    opacity: 1;
    transform: translateY(0);
  }
  &.react-transition-group-exit-active {
    opacity: 0;
    transform: translateY(-20%);
    transition-duration: 150ms;
    transition-property: opacity, transform;
    transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
  }
`;

const image = css`
  background-color: #eee;
  border-radius: 4px;
  display: block;
`;

const button = css`
  align-self: flex-start;
  background-color: #61dafb;
  border: none;
  border-radius: 8px;
  color: #212121;
  cursor: pointer;
  font-size: 20px;
  font-weight: bold;
  justify-self: center;
  padding: 20px 40px;
  text-decoration: none;
  width: 200px;
`;

export default App;

ポイントは<CSSTransition>に渡したclassNameは1つ下の要素に受け継がれつつ、状態に合わせて色々なサフィックスがつくことです。

const someComponent = () => {
  return (
    <CSSTransition className="fade">
      <div>I'm component</div>
    </CSSTransition>
  )
}

上記のようにした場合divに対して、状態に合わせて以下のクラスがついたり消えたりします。

  • appear系
    • fade-appear
    • fade-appear-active
    • fade-appear-done
  • enter系
    • fade-enter
    • fade-enter-active
    • fade-enter-done
  • exit系
    • fade-exit
    • fade-exit-active
    • fade-exit-done

今回の例では以下の4つのものを使いました。

  • className-enter
    • 今まさにマウントされた!という瞬間につくクラス
  • className-enter-active
    • マウントされて、出現するアニメーションをしている最中につくクラス
  • className-exit
    • 今からアンマウントされる!という瞬間につくクラス
  • className-exit-active
    • アンマウントされるにあたって消えゆくアニメーションをしている最中につくクラス

それ以外にも例えばフェードインで出現し、アニメーションが終わったら更に背景色が変わって欲しい、とあらばclassName-enter-doneのクラスに対してCSSを指定します。

まとめ

  • Reactにおいて、他のライブラリに頼らずアンマウント時のアニメーションを書くのは大変
  • React Transition Groupを使えばアンマウントが始まる瞬間や消えゆく時間、消えきった瞬間などタイミングを細かく指定してスタイリングできる
  1. もっと良いやり方があるかもしれません。もしあったら是非教えてください :pray:

7
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?