前回の続き
ページ遷移時にアニメーションをつけたいと思って、いろいろ探しているうちにReact Transition Groupへたどり着きました。
実現するまでに躓いたこと
useState,useEffectで切り替え → ✕
CSSのクラスを定義しておいてuseStateで切り替えればいいのでは?と考えました。
import type { NextPage } from 'next'
import { useState, useEffect } from 'react'
import { css } from '@emotion/css'
const bg = css`
background-color: black;
`
const Home: NextPage = () => {
const [isState,setIsState] = useState(false)
useEffect(()=>{
return (()=> setIsState(true))
},[])
return (
<div className={isState ? bg : ""}>aaa</div>
)
}しかし、上記コードは2重の理由で実現できません。
1.useEffectのクリーンアップ関数はcomponentWillUnmountではない
正確に理解していなかったし、現時点でも理解が怪しいと思いますが、一旦知識のアップデートプロセスは下記のようになっています。
useEffectののクリーンアップ関数(return)はcomponentWillUnmount、つまりコンポーネントが無くなる時に処理を解除する、、、と思っていた。- 解除する処理を記述するということは、「何かしら処理を行うことができる」ということ・・・!
- 決定的な理由はないが、何でもしていいわけではないよな…という認識もあった。
- 自身の考えはどうあれ、
useEffectのクリーンアップ関数(return)はunmountではなくmount時にも1度呼ばれることがわかった。 - つまりサンプルコードでは下記の動きになる
useState(false)で初期化しているが、useEffectでreturn(()=>setIsState(true)しているので- 最終的には
isState = trueとなっているので、 classNameにはbgがセットされてしまう
2.useStateを2種類管理して切り替える
じゃあ別のuseState用意して変更を検知したらisStateを切り替えて、classNameも付与されるようなことを考えたが、そもそもこの考え方に欠陥がある。
unmount時にstateを切り替えようとしている- 切り替えたところで
unmount時には再レンダリングが行われないのでDOMは切り替わらない
SPAで考える
SPAならコンポーネントのアンマウント処理ができるのではないかと思ったが、元々SSGで作ろうとおもっていたため、Next.jsで一部SPAにできないか考えてみた。
Linkタグを用いるとSSR時でもブラウザリロードすることなくコンポーネントを再レンダーすることができる。
Linkタグで再レンダリングは可能みたいだが、レンダリングするまでの間にアニメーションを挟みたいので、Next.jsのルーティングについて確認してみた。すると、next/linkではなくnext/routerのuseRouterフックを使えば間に処理を挟めそうだった。
参考:useRouter(公式)
アンマウント時のアニメーション実装方法
アンマウント時のアニメーションを実現する方法を模索している段階でReact Transition Groupのライブラリの存在を知ったのですが、調べてみると今回の目的をこのライブラリで果たせそうです。
参考:React Transition Group(公式)
React Transition Groupについて
React Transition Groupはアニメーションを管理するライブラリで、アニメーションのCSSそのものは自身で記述します。React Transition Groupでは4つのコンポーネントが提供されていますが、今回はその中でCSSTransitionを使っていきます。
CSSTransitionコンポーネント実装
CSSTransitionの使い方や説明まで書くと記事が長くなってしまうので、参考記事を最後にまとめてあるのでそちらを参考にしてください。本記事の冒頭で紹介したコードを元に書き換えていきます。
// .styles/globals.scss
@forward "styles.css";// .styles/styles.scss
.bg {
&-exit-done {
background-color: black;
}
&-appear {
background-color: yellow;
}
&-appear-done {
background-color: #fff;
}
}まずCSSを定義します。クラス名の説明については後述します。ちなみに/pageのファイルに直接import './styles/styles.scss'とかいたら怒られました。副作用等の問題でやってはいけないらしい。
Global CSS cannot be imported from files other than your Custom <App>. Due to the Global nature of stylesheets, and to avoid conflicts, Please move all first-party global CSS imports to pages/_app.js. Or convert the import to Component-Level CSS (CSS Modules).
Read more: https://nextjs.org/docs/messages/css-global次にページを修正します。
// pages/index.tsx
import type { NextPage } from 'next'
import { useState } from 'react'
import { CSSTransition } from 'react-transition-group'
import { useRouter } from 'next/router'
const Home: NextPage = () => {
const [isState,setIsState] = useState(true)
const router = useRouter()
const handleClick = () => {
setIsState(false)
setTimeout(() => {
router.push('/sub')
}, 1000)
}
return (
<CSSTransition in={isState} timeout={1000} classNames="bg">
<button onClick={handleClick}>link</button>
</CSSTransition>
)
}
export default Home// pages/sub.tsx
import type { NextPage } from 'next'
import { CSSTransition } from 'react-transition-group'
const Sub: NextPage = () => {
return (
<CSSTransition in={true} appear={true} timeout={1000} classNames="bg">
<p>sub page</p>
</CSSTransition>
)
}
export default Sub
ページ遷移時のアニメーションの実装ができました。
解説
ページ遷移元のindex.tsxのCSSTransitionコンポーネントには3つの属性が設定されています。
- in :
trueorfalseでアニメーションの状態を指定します - timeout : アニメーションが変化するまでの時間
- classNames : 指定したクラス名とin propsの組み合わせで、アニメーションのクラス名が付与されます。
isStateの初期値はuseState(true)です。trueからfalseに変わる時、classNamesで指定したクラス名と組み合わせて以下のように変化します。
- 変化前:クラス名なし
- 変化中:bg-exit, bg-exit-active
- 変化後:bg-exit-done
styles.scssにはbg-exit-doneのスタイルが定義されているので、変化後にbg-exit-doneのスタイルがあたります。サンプルコードではtimeoutと同じ時間をsetTimeoutで待ってからページ遷移するようにしています。
次にページ遷移先のsub.tsxではappearの属性が追加されています。in={true}かつappear={true}とすると、マウント時のアニメーションを定義できます。クラス名が変化する流れはexitの時とあまり変わりません。
- ページ遷移する
- bg-appear, bg-appear-active
- bg-appear-done
今回のサンプルコードではbackground-color: yellowからbackground-color: #fffに変化するようにしています。