これは何
- 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を使えばアンマウントが始まる瞬間や消えゆく時間、消えきった瞬間などタイミングを細かく指定してスタイリングできる
-
もっと良いやり方があるかもしれません。もしあったら是非教えてください ↩