2022-09-26

Next.jsでWebサイトを作る④〜ページ遷移時のアニメーション

前回の続き
ページ遷移時にアニメーションをつけたいと思って、いろいろ探しているうちに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)で初期化しているが、
    • useEffectreturn(()=>setIsState(true)しているので
    • 最終的にはisState = trueとなっているので、
    • classNameにはbgがセットされてしまう

2.useStateを2種類管理して切り替える

じゃあ別のuseState用意して変更を検知したらisStateを切り替えて、classNameも付与されるようなことを考えたが、そもそもこの考え方に欠陥がある。

  • unmount時にstateを切り替えようとしている
  • 切り替えたところでunmount時には再レンダリングが行われないのでDOMは切り替わらない

SPAで考える

SPAならコンポーネントのアンマウント処理ができるのではないかと思ったが、元々SSGで作ろうとおもっていたため、Next.jsで一部SPAにできないか考えてみた。

Linkタグを用いるとSSR時でもブラウザリロードすることなくコンポーネントを再レンダーすることができる。

引用:Next.jsにおけるSPA的遷移の考え方

Linkタグで再レンダリングは可能みたいだが、レンダリングするまでの間にアニメーションを挟みたいので、Next.jsのルーティングについて確認してみた。すると、next/linkではなくnext/routeruseRouterフックを使えば間に処理を挟めそうだった。 参考: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

CSSTransition webp

ページ遷移時のアニメーションの実装ができました。

解説

ページ遷移元のindex.tsxCSSTransitionコンポーネントには3つの属性が設定されています。

  • in : true or falseでアニメーションの状態を指定します
  • timeout : アニメーションが変化するまでの時間
  • classNames : 指定したクラス名とin propsの組み合わせで、アニメーションのクラス名が付与されます。

isStateの初期値はuseState(true)です。trueからfalseに変わる時、classNamesで指定したクラス名と組み合わせて以下のように変化します。

  1. 変化前:クラス名なし
  2. 変化中:bg-exit, bg-exit-active
  3. 変化後:bg-exit-done

styles.scssにはbg-exit-doneのスタイルが定義されているので、変化後にbg-exit-doneのスタイルがあたります。サンプルコードではtimeoutと同じ時間をsetTimeoutで待ってからページ遷移するようにしています。

次にページ遷移先のsub.tsxではappearの属性が追加されています。in={true}かつappear={true}とすると、マウント時のアニメーションを定義できます。クラス名が変化する流れはexitの時とあまり変わりません。

  1. ページ遷移する
  2. bg-appear, bg-appear-active
  3. bg-appear-done

今回のサンプルコードではbackground-color: yellowからbackground-color: #fffに変化するようにしています。

参考