🔗

Next.js でページ遷移前後で共通するコンポーネントを残す

5 min read

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 アプリの外に遷移する

一般的な Router ライブラリと同じく(?)、Link component [3]push 関数[4]の2種類の API が提供されているが、これらに振る舞い上の違いはほぼない(はず)。用意されてるオプションもだいたい同じ(たぶん)。

  • 異なる page component に遷移する場合、元の page component は unmount される
    • 当然ながら、前後の page の下に同じコンポーネントが存在していても unmount される
  • 同じ page component に遷移する場合、page component の unmount は発生しない
    • これは dynamic route([path].tsx みたいなやつ)も同じ

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 さわるひとはみんなドキュメント全部読みましょう(自分への戒め)。

脚注
  1. Basic Features: Layouts | Next.js ↩︎

  2. Next.js のドキュメント上では「input values, scroll poistion 等のページの状態を持続させる」 ことを指して SPA experience と表現していた / Basic Features: Layouts | Next.js ↩︎

  3. next/link | Next.js ↩︎

  4. next/router | Next.js ↩︎

  5. Routing: Shallow Routing | Next.js ↩︎

  6. レイアウトとレンダリング - Railsガイド ↩︎

  7. ちなみに、Rails のレイアウトであれば、Controller 内で定義されたインスタンス変数を読みに行けるので問題は起きない(これはこれで、データの依存関係が見づらくなるので難しいけど…)。 ↩︎

Discussion

ログインするとコメントできます