Next.js でページ遷移前後で共通するコンポーネントを残す
TL;DR
- Next.js アプリ内のページ遷移では、原則として元 page の component は unmount される
- 「遷移前後の page component が同じである」場合、および「
App
に記述されたコンポーネント(Layout)」は unmount されない -
page component に
getLayout
関数を定義するパターン[1]を適用することで、page ごとの Layout を細かく設定・管理できる
モチベーション
たとえばタブを使ったページ遷移などで、一部のコンポーネントを引き継ぎたい場合。
zenn.dev でいうとトップページのグローバルナビゲーション直下のタブ。
このタブを切り替えるときに、グローバルナビゲーションやタブは画面に残っていてほしい[2]。
普通にやるとタブが unmount されるので、ユーザから見ると画面が一瞬チラつく場合がある。
とくに画像とかが埋まっていると目立つ。
(zenn.dev ではこのコンポーネントはちゃんと画面に残り続けている)
前提: Next.js におけるページ遷移の実装と挙動
<a href="">
- ブラウザレベルの再読み込みが発生する
- Next.js アプリの外に遷移する
(next/link).Link
component, (next/router).Router.push
一般的な Router ライブラリと同じく(?)、Link
component [3] と push
関数[4]の2種類の API が提供されているが、これらに振る舞い上の違いはほぼない(はず)。用意されてるオプションもだいたい同じ(たぶん)。
- 異なる page component に遷移する場合、元の page component は unmount される
- 当然ながら、前後の page の下に同じコンポーネントが存在していても unmount される
- 同じ page component に遷移する場合、page component の unmount は発生しない
- これは dynamic route(
[path].tsx
みたいなやつ)も同じ
- これは dynamic route(
shallow
[5] というオプションがあり、これを有効化すると同じ page component に遷移する場合に限り遷移時に getServerSideProps
が呼ばれなくなる。
遷移時の getServerSideProps について
逆に言うと、「異なる page component に遷移する」もしくは「shallow
なしで遷移する」場合は getServerSideProps
が実行される。当然サーバ側で実行されるが、これは HTTP GET リクエスト経由で呼び出され、返り値が JSON で返却される。
# /routes/foo への遷移で叩かれる URL
http://localhost:3000/_next/data/TQ9O89_OyoosCxqGxluRO/routes/foo.json
{"pageProps":{},"__N_SSP":true}
HTML を取得し直してるとかではないので、ブラウザの unload も発生しない。
挙動のまとめ
Next.js の API で、遷移先やオプションによる挙動の違いをまとめる。
| | unload
event | getServerSideProps
| 共通 component (App) | 共通 component (page) |
| --- | --- | --- | --- | --- | --- |
| 異なる page / shallow なし | 起きない | 呼ばれる | unmount されない | unmount される |
| 異なる page / shallow あり | 起きない | 呼ばれる | unmount されない | unmount される |
| 同一 page / shallow なし | 起きない | 呼ばれる | unmount されない | unmount されない |
| 同一 page / shallow あり | 起きない | 呼ばれない | unmount されない | unmount されない |
ページ遷移前後で共通するコンポーネントを unmount させないためには
前節の表から、ページ遷移前後で共通するコンポーネントを unmount させたくない場合は「共通の page component にする」もしくは「App
で render する」のいずれかにする必要がある。
が、前者は広く使われるコンポーネントや複雑なナビゲーションで適用しづらい。
後者は一握りのページでしか使われないものとかが入ってくるとカオスになるのが目に見えている。
じゃあどうするか?
getLayout
- ページごとのレイアウトを定義できるようにする
たとえば Ruby on Rails なら View を「レイアウト」でラップすることができ、かつそのレイアウトを Controller や Action ごとに設定できるようになっている[6]。
このレイアウト同じようなことをする例が、 Next.js の Basic Features: Layouts で紹介されている。
最も単純には、pages
ディレクトリの外に定義した自作の Layout
component を custom App
で利用する。
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
ページごとにレイアウトを設定できるようにするには、ページコンポーネントに getLayout
という関数をもたせ、それを custom App
で呼び出す。
// from: https://nextjs.org/docs/basic-features/layouts#per-page-layouts
// pages/index.ts
Page.getLayout = function getLayout(page: ReactElement) {
return (
<Layout>
<NestedLayout>{page}</NestedLayout>
</Layout>
)
}
// pages/_app.js
// AppPropsWithLayout の定義は元記事を参照
export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout || ((page) => page)
return getLayout(<Component {...pageProps} />)
}
先の挙動まとめの通り、App
は Next.js 内の遷移は unmount されることはない。よって、この getLayout()
のパターンを上手く実装することで、「ページ遷移前後で共通するコンポーネントを残す」というのを上手く実現できる。
Layout で props を要求したいときは?
前節の getLayout
のコードでは getLayout
は page component の ReactElement
を要求しているため、Layout に特殊な props を渡すのが難しい。どうしよう?[7]
単純に解決するのであれば、getLayout
の引数に pageProps を追加してしまうのが早そう。
// getServerSideProps の返り値の型から Props の型を推論するヘルパ
// https://nextjs.org/docs/basic-features/data-fetching#typescript-use-getserversideprops
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
export default Page(props: Props) {
return (
// ...
)
}
// pages/index.ts
Page.getLayout = function getLayout(page: ReactElement, props: Props) {
return (
// ...
)
}
とはいえ、page component と密に連携するような複雑なコンポーネントを getLayout
におしこめていいかどうかは悩ましいポイントではある…。
おわりに
この記事にまとめてる内容はだいたい Next.js の公式ドキュメントに書いてあったので、Next.js さわるひとはみんなドキュメント全部読みましょう(自分への戒め)。
-
Next.js のドキュメント上では「input values, scroll poistion 等のページの状態を持続させる」 ことを指して SPA experience と表現していた / Basic Features: Layouts | Next.js ↩︎
-
ちなみに、Rails のレイアウトであれば、Controller 内で定義されたインスタンス変数を読みに行けるので問題は起きない(これはこれで、データの依存関係が見づらくなるので難しいけど…)。 ↩︎
Discussion