🐱

[Next.js] App Router環境下のページ毎のレンダリング形式の設定方法

2024/02/27に公開

概要

今回は、最近実務の中で調査を行った App Router 環境下でのページ毎のレンダリング形式の設定方法について、まとめていこうと思います。
具体的には、Pages Router の環境と比べて、ページに対しての SSR と SSG の設定方法と、SSG での細かな挙動の設定方法などが異なっていたため、その辺りについての記載を行なっていこうと思います。

https://nextjs.org/docs/app

公式のドキュメントを参照しながらまとめたため、大きな解釈違いはないと思っているのですが、間違っている箇所などあれば気軽にご指摘いただけると嬉しいです。

今まで(Pages Router)のレンダリング形式の設定

  1. getServerSideProps や getStaticProps で SSR や SSG の設定が行える
  2. getStaticProps は getStaticPaths で細かな定義が行えます
  3. getStaticPaths の fallback で SSG のパスがなかった時の設定を行えます

SSR, SSG の設定方法

まず、今までの Pages Router 環境でのレンダリング形式の設定方法を見ていこうと思います。
方法としてはシンプルで、ページ毎に SSR であれば getServerSideProps を定義、SSG であれば getStaticProps を定義する形になります。

// SSR
import type { InferGetServerSidePropsType, GetServerSideProps } from 'next';

type Repo = {
  name: string
  stargazers_count: number
}

export const getServerSideProps: GetServerSideProps<{ repo: Repo }> = (async () => {
  const res = await fetch('https://api.github.com/repos/vercel/next.js');
  const repo: Repo = await res.json();
  return { props: { repo } };
});

export default function Page({ repo }: InferGetServerSidePropsType<typeof getServerSideProps>) {
  return (
    <main>
      <p>{ repo.stargazers_count }</p>
    </main>
  );
}
// SSG
import type { InferGetStaticPropsType, GetStaticProps } from 'next';

type Repo = {
  name: string
  stargazers_count: number
}

export const getStaticProps: GetStaticProps<{ repo: Repo }> = (async () => {
  const res = await fetch('https://api.github.com/repos/vercel/next.js');
  const repo = await res.json();
  return { props: { repo } };
});

export default function Page({ repo }: InferGetStaticPropsType<typeof getStaticProps>) {
  return repo.stargazers_count;
}

Next.js の公式のコードをほとんどそのまま参照させていただきましたが、レンダリング形式の選択は非常に直感的で、今まで Pages Router で開発を行ってきた方なら見慣れた実装になっています。

https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props

https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-props

SSG + Dynamic Routes

合わせて確認しておきたいところとしては、動的なページを使用する場合です。
具体的には、getStaticProps(SSG) + Dynamic Routes を行う場合、静的に生成するパスを定義する必要があります。(前提として、Dynamic Routes を使用するページは SSR でレンダリングされます)

実装方法としてはこちらもシンプルで、getStaticPathsすることで実装可能となっています。

// SSG + Dynamic Routes
import type { InferGetStaticPropsType, GetStaticProps, GetStaticPaths } from 'next';
 
type Repo = {
  name: string
  stargazers_count: number
}
 
export const getStaticPaths: GetStaticPaths = (() => {
  return {
    paths: [
      {
        params: {
          name: 'next.js',
        },
      },
    ],
    fallback: true,
  };
});
 
export const getStaticProps: GetStaticProps<{ repo: Repo }> = (async () => {
  const res = await fetch('https://api.github.com/repos/vercel/next.js')
  const repo = await res.json();
  return { props: { repo } };
});
 
export default function Page({ repo }: InferGetStaticPropsType<typeof getStaticProps>) {
  return repo.stargazers_count;
}

https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-paths

SSG + Dynamic Routes + fallback

最後に、SSG + Dynamic Routes の getStaticPaths に定義できる fallback という値について説明しておこうと思います。

こちらの値については、true false 'blocking'という3つの値で管理することができ、それぞれ getStaticProps に対しての挙動の設定を行うことができます。

それぞれの値に対しての挙動は以下のようになっており、主な役割としては「getStaticPaths 内で定義したパス以外のパスでリクエストされた際の挙動」を行っています。

fallback: true
getStaticPaths で定義したパス以外でリクエストされた際、404ページは返されず、描画が行われます。
しかし、本来静的に生成する際に取得しているはずのデータは持ち得ないため、そのデータはクライアント側で取得が行われる形になります。

fallback: false
getStaticPaths で定義したパス以外でリクエストされた際、404ページが返されます。

fallback: 'blocking'
getStaticPaths で定義したパス以外でリクエストされた際、404ページは返されず、描画が行われます。
しかし、本来静的に生成する際に取得しているはずのデータは持ち得ないため、そのページ自体が SSR で生成され、クライアント側に返されることになります。

// 省略

export const getStaticPaths: GetStaticPaths = (() => {
  return {
    paths: [
      {
        params: {
          name: 'next.js',
        },
      },
    ],
    fallback: true,   // true or false or blocking
  };
});

// 省略

https://nextjs.org/docs/pages/api-reference/functions/get-static-paths#getstaticpaths-return-values

これから(App Router)のレンダリング形式の設定

  1. dynamic という値で SSR や SSG の管理が行える
  2. generateStaticParams を定義することで Dynamic Routes で SSG にできる
  3. generateStaticParams が getStaticPaths の役割を担う
  4. dynamicParams で SSG のパスがなかった時の設定を行えます

SSR, SSG の設定方法

本題である App Router 環境でのレンダリングの設定方法を見ていこうと思います。
方法としては、Pages Router と同様に、ページ毎に値を追加し、制御を行う形になります。しかし、Pages Router 環境とは定義する形式が異なっており、dynamicという値で制御を行います。

export const dynamic = 'auto'
// 'auto' | 'force-dynamic' | 'force-static' | 'error'

この dynamic という値を layout.tsx や page.tsx で定義を行うことで、それぞれのページのレンダリング方法を決めることができます。

export const dynamic = 'auto'
デフォルトで設定されている値であり、ページ全体としてレンダリング方法を定義しない形になります。
この場合、ページのレンダリング方法の判定としては、ページ内で行っている fetch 関数の cache 形式により決定され、cache の値を指定しなかった場合は事前に生成される形になります。

export default async function Page() {
  // SSG で判定される形のキャッシュ
  // force-cache はデフォルトで指定されているため、値を指定しなくても問題ない
  const staticData = await fetch(`https://...`, { cache: 'force-cache' })

  // SSR で判定される形のキャッシュ
  const dynamicData = await fetch(`https://...`, { cache: 'no-store' })
 
  return <div>...</div>
}

https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration#step-6-migrating-data-fetching-methods

export const dynamic = 'force-dynamic'
この値を設定することで、ページを SSR としてレンダリングさせることができます。

export const dynamic = 'force-static'
この値を設定することで、ページを SSG としてレンダリングさせることができます。

export const dynamic = 'error'
この値を設定することで、ページを SSG としてレンダリングさせることができます。
しかし、この値を指定した上で SSR と判定させる挙動(dynamic functionなど)を行った場合、エラーを発生させ、強制的に SSG として判定されます。

https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic

SSG + Dynamic Routes

次に、SSG + Dynamic Routes を実装する場合を見ていきます。
方法としては、generateStaticParams が Pages Router 環境下での getStaticPaths の役割を担っており、Dynamic Routes での静的に生成するパスを定義することができます。

export async function generateStaticParams() {
  const posts = await fetch('https://.../posts').then((res) => res.json())
 
  return posts.map((post) => ({
    slug: post.slug,
  }))
}

この generateStaticParams 関数も dynamic の値と同様に、layout.tsx や page.tsx に定義することで静的であると明示的にすることができます。

https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes#generating-static-params

https://nextjs.org/docs/app/api-reference/functions/generate-static-params

SSG + Dynamic Routes + fallback

最後に、Pages Router 環境下での SSG + Dynamic Routes + fallback について見ていきたいと思います。

App Router 環境の generateStaticParams には、getStaticPaths にあった fallback の値はなく、別で定義する必要があります。そして、その別の値というのが、dynamicParamsであり、boolean で管理することができます。

// page.tsx
export const dynamicParams = true // true | false

この dynamicParams も dynamic や generateStaticParams 関数と同様に、layout.tsx や page.tsx に定義することができ、静的に生成したパスに含まれないパスがリクエストされた際の挙動を明示的にすることができます。

export const dynamicParams = true
generateStaticParams で定義したパス以外でリクエストされた際、404ページは返されず、描画が行われます。
しかし、本来静的に生成する際に取得しているはずのデータは持ち得ないため、そのページ自体が SSR で生成され、クライアント側に返されることになります。

export const dynamicParams = false
generateStaticParams で定義したパス以外でリクエストされた際、404ページが返されます。

https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams

補足として、Pages Router 環境の fallback では、3つの値(true, false, blocking)で管理していましたが、App Router 環境の dynamicParams では、2つの値(true, false)になっています。

この背景について公式に記載があり、「fallback の値が true と blocking の時と違って、dynamicParams の値が true の場合は、ストリーミングで返されるため、true と blocking の両方を兼ねる形の実装になっているよ」みたいなことが書かれていました。

This replaces the fallback: true | false | 'blocking' option of getStaticPaths in the pages directory. The fallback: 'blocking' option is not included in dynamicParams because the difference between 'blocking' and true is negligible with streaming.

まとめ

今回の記事では、Pages Router と App Router のそれぞれのレンダリング形式の設定方法をまとめてきました。思っていた以上に変化が大きく、やはり App Router を何となくで使い始めるのは危険だなぁということを感じさせられました。

最後に、ここまで Pages Router と App Router を比較する形で記事を書いてきましたが、個人的な解釈で書いてしまっているところも少しながらあったりします。できるだけ公式の言っていることを参考にまとめましたが、間違っている箇所があれば優しく指摘いただけると嬉しいです。

Discussion