前回の続き
ページ遷移時にアニメーションをつけたいと思って、いろいろ探しているうちに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 :
true
orfalse
でアニメーションの状態を指定します - 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
に変化するようにしています。