Next.js + Framer Motion でページ遷移アニメーションを実装する

# まえがき

React のアニメーションライブラリである [Framer Motion](https://www.framer.com/motion/) を利用してページ遷移アニメーションを実装した際に、いくつか引っかかった点があったので、備忘録として残しておこうと思います。

# バージョン

- Next.js v12.1.1
- Framer Motion v7.6.1

# 今回実装するもの

![anim.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286055/cdf686d5-f928-f1dd-8a55-7137037b3f57.gif)

↑ こんな感じのふんわりしたフェードイン / フェードアウトを実装します。

# 手順

## 1. アンマウント時のアニメーションを有効にする

```tsx:_app.tsx
import { AnimatePresence } from 'framer-motion'
import type { AppProps } from 'next/app'

import 'styles/globals.css'

const MyApp = ({ Component, pageProps, router }: AppProps) => (
  <AnimatePresence mode="wait">
    <Component key={router.asPath} {...pageProps} />
  </AnimatePresence>
)

export default MyApp
```

アンマウント時のアニメーションを有効にするため、`framer-motion` から `<AnimatePresence>` を import して、`<Component>` を囲みます。

### AnimatePresence

`mode="wait"` とすることで、コンポーネントのアンマウントを待ち、遷移元のアンマウント時のアニメーションと遷移先のマウント時のアニメーションが同時に発生しないようにしています。

### Component

`key` の値として、初めは `router.route` を利用していたのですが、この値は `pages/` ディレクトリ以下のファイルパス **そのもの** であり、 [Dynamic Routing](https://nextjs.org/docs/routing/dynamic-routes) を使用して生成しているページ（`[id].tsx` みたいなページ）では `[id]` の部分が展開されず同じ値になってしまうので、「一部ページでアニメーションが再生されない」という問題が起きました。

ですので、ここではブラウザ上で表示されるパスである `router.asPath`を利用しています。

```js:こんな感じ
// パス = pages/[id]
// id = hoge

// ...

console.log(router.route) // -> "pages/[id]"
console.log(router.asPath) // -> "pages/hoge"
```

## 2. 遷移アニメーションを設定

```tsx:hoge.tsx
import { motion } from 'framer-motion'
import type { NextPage } from 'next'

const Hoge = (): NextPage => (
  <motion.div
    initial={{ opacity: 0 }} // 初期状態
    animate={{ opacity: 1 }} // マウント時
    exit={{ opacity: 0 }}    // アンマウント時
  >
   // ...
  </motion.div>
)

export default Hoge
```

全体を `<motion>` コンポーネントで囲み、任意の遷移アニメーションを設定します。

詳しい Props については、[Motion components](https://www.framer.com/docs/component/) をご覧ください...。

## 3. ページ遷移時の挙動を修正

![bug.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286055/55da2490-dfcf-446e-90a3-e63d3f369b8e.gif)

ここまででアニメーション自体は実装できているのですが、スクロールしてからページ遷移を行うと、「アニメーション再生前にページが先頭に戻る」という挙動になってしまいます。

これでは少し不恰好なので、修正していきます。

### 1. リンクを修正

```diff_tsx:fuga.tsx
import Link from 'next/link'

const Fuga = (): JSX.Element => (
  <div>
-   <Link href="https://example.com/">
+   <Link href="https://example.com/" scroll={false}>
      リンク！
    </Link>
  </div>
)

export default Fuga
```

ページの遷移が発生する箇所の `<Link>` コンポーネントに `scroll={false}` を追加して、遷移後の自動スクロールを無効化します。

### 2. _app.tsxを修正

```diff_tsx:_app.tsx
import { AnimatePresence } from 'framer-motion'
import type { AppProps } from 'next/app'

import 'styles/globals.css'

const MyApp = ({ Component, pageProps, router }: AppProps) => (
- <AnimatePresence mode="wait">
+ <AnimatePresence mode="wait" onExitComplete={() => window.scrollTo(0, 0)}>
    <Component key={router.asPath} {...pageProps} />
  </AnimatePresence>
)

export default MyApp
```

`onExitComplete={() => window.scrollTo(0, 0)}` を追加して、アンマウント完了後にページを先頭まで戻すようにします。

![good.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286055/0167e5da-b3c8-f248-b406-95fbc176f090.gif)

これでOKです！ 🎉

# あとがき

Framer Motion について、かなりシンプルな記述でアニメーションを実装することができ、とても楽しいなという印象を持ちました。

2Dアニメーションだけでなく [3Dアニメーション](https://www.framer.com/docs/three-introduction/) も扱えたりするようなので、もう少し色々触ってみようと思います！

https://github.com/arrow2nd/arrow2nd.com-gen1

# 参考

- [Animating Next.js page transitions with Framer Motion - DEV Community](https://dev.to/jameswallis/animating-next-js-page-transitions-with-framer-motion-1g9j)
- [Next.js+framer-motionで画面遷移アニメーション - Qiita](https://qiita.com/Pretend16474300/items/6e49a6b33dde999f28c6)
- [かっこいいアニメーションを実装できるFramer Motionを触ってみた - KAYAC engineers' blog](https://techblog.kayac.com/framer-motion-playground)