Next.js CSR / SSR / SSG / ISR の実装方法

# はじめに
Next.js では、2023年7月時点で以下2つのルーティング方法があります。
- [App Router](https://nextjs.org/docs/app)：Next.js 13から利用可能。現在はこちらが推奨。
- [Pages Router](https://nextjs.org/docs/pages)：Next.js 13より前はこちらが主流。

App Router と Pages Router のどちらを利用するかによって、レンダリング手法等その他機能の実装も変わってきます。

App Router と Pages Router のレンダリングの違いを整理したいので、まずこの記事では、Pages Router で利用できる以下4つのレンダリングの実装方法について学んでいきたいと思います。

- Client-Side Rendering (CSR)
- Server-Side Rendering (SSR)
- Static-Site Generation (SSG)
- Incremental Static Regeneration (ISR)

※各レンダリング手法の仕組みやメリット・デメリット、ユースケースなどについては、別途こちらの記事でまとめています。

https://qiita.com/Yasushi-Mo/items/712e2d8404e6fc1939a0

# 開発環境
開発環境は以下の通りです。
- Windows11
- VSCode
- Next.js 13.4.7
- React 18.2.0
- TypeScript 5.1.3

# プロジェクト作成
まずは以下のコマンドを実行して、プロジェクトを作成します。

```
npx create-next-app@latest
```

インストール中にプロンプトが表示されるので、それぞれ以下の値を入力・選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/913193f1-0da8-1b38-dae3-e48601996a76.png)

:::note
今回は Pages Router を使いたいので、```Would you like to use App Router? (recommended)``` は ```No``` を選択します。それ以外は、デフォルト値通りにします。
:::

インストールが完了するとプロジェクトファイルが作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/aa63cf2e-9ed7-d063-411a-03cf5e7b62d7.png)

以下のコマンドでローカルサーバーを起動し、http://localhost:3000 にアクセスするとアプリが表示されます。
```
npm run dev
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/7fbaa038-0cb4-47e2-bde2-29c3c033ec1e.png)

# 実装するサンプルアプリ
以下のAPIから取得したデータを表示するアプリを作成します。

https://official-joke-api.appspot.com/random_joke

# 関連ページ・コンポーネントの追加
```pages``` フォルダ以下に各レンダリング手法の名前をつけたページを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/41cb5e64-da17-228c-6ba0-37164d232b5d.png)

次に ```src/pages/index.tsx``` を書き換えて、先ほど追加した各ページに遷移できるようにします。

```src/pages/index.tsx
import { Inter } from "next/font/google";
import { TITLE as TITLE_CSR } from "./client-side-rendering";
import { TITLE as TITLE_SSR } from "./server-side-rendering";
import { TITLE as TITLE_SSG } from "./static-site-generation";
import { TITLE as TITLE_ISR } from "./incremental-static-regeneration";
import Link from "next/link";

const inter = Inter({ subsets: ["latin"] });

export default function Home() {
  return (
    <main
      className={`flex min-h-screen flex-col items-center justify-between p-24 ${inter.className}`}
    >
      <h1>Next.js Pages Router</h1>

      <div className="mb-32 grid text-center lg:mb-0 lg:grid-cols-4 lg:text-left">
        <Link
          href="/client-side-rendering"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          rel="noopener noreferrer"
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            {TITLE_CSR}{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
        </Link>

        <Link
          href="/server-side-rendering"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          rel="noopener noreferrer"
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            {TITLE_SSR}{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
        </Link>

        <Link
          href="/static-site-generation"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          rel="noopener noreferrer"
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            {TITLE_SSG}{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
        </Link>

        <Link
          href="/incremental-static-regeneration"
          className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
          rel="noopener noreferrer"
        >
          <h2 className={`mb-3 text-2xl font-semibold`}>
            {TITLE_ISR}{" "}
            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
              -&gt;
            </span>
          </h2>
        </Link>
      </div>
    </main>
  );
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/7e0b5a7e-a109-283a-6bd9-49326beb51a2.png)


最後にAPIから取得したデータを表示するコンポーネントを追加します。
このコンポーネントは全てのページで利用します。

```FetchedRandomJoke.tsx
import { RandomJoke } from "@/types";

export function FetchedRandomJoke({
  randomJoke,
}: {
  randomJoke: RandomJoke | undefined;
}) {
  if (!randomJoke) return <p>Loading...</p>;

  return (
    <>
      <hr />
      <p>Fetched Data from Random Joke API</p>
      <ul>
        <li>Type: {randomJoke.type}</li>
        <li>Setup: {randomJoke.setup}</li>
        <li>Punchline: {randomJoke?.punchline}</li>
      </ul>
    </>
  );
}

```

# Client-Side Rendering (CSR) の実装
データ取得ライブラリである SWR を利用してAPIからデータを取得します。

https://swr.vercel.app/ja

```client-side-rendering.tsx
import { FetchedRandomJoke } from "@/components/FetchedRandomJoke";
import { SAMPLE_API_ENDPOINT } from "@/pages/api";
import { RandomJoke } from "@/types";
import Head from "next/head";
import useSWR from "swr";

export const TITLE = "Client-Side Rendering (CSR)";

export default function ClientSideRendering() {
  const { data: randomJoke } = useSWR<RandomJoke>(SAMPLE_API_ENDPOINT, fetcher);

  return (
    <>
      <Head>
        <title>{TITLE}</title>
      </Head>
      <h1>{TITLE}</h1>
      <p>Data fetched on the client-side only.</p>
      <FetchedRandomJoke randomJoke={randomJoke} />
    </>
  );
}

async function fetcher() {
  const response = await fetch(SAMPLE_API_ENDPOINT);
  const randomJoke = await response.json();

  return randomJoke;
}
```

# Server-Side Rendering (SSR) の実装
```getServerSideProps``` という非同期関数を利用してAPIからデータを取得します。

```server-side-rendering.tsx
import Head from "next/head";
import { SAMPLE_API_ENDPOINT } from "./api";
import { FetchedRandomJoke } from "@/components/FetchedRandomJoke";
import { RandomJoke } from "@/types";

export const TITLE = "Server-Side Rendering (SSR)";

export default function ServerSideRendering({
  randomJoke,
}: {
  randomJoke: RandomJoke;
}) {
  return (
    <>
      <Head>
        <title>{TITLE}</title>
      </Head>
      <h1>{TITLE}</h1>
      <p>
        Data fetched on the server-side at <b>each</b> request before sending to
        the client.
      </p>
      <FetchedRandomJoke randomJoke={randomJoke} />
    </>
  );
}

export async function getServerSideProps() {
  const response = await fetch(SAMPLE_API_ENDPOINT);
  const randomJoke = await response.json();

  return {
    props: {
      randomJoke, // will be passed to the page component as props
    },
  };
}
```

# Static-Site Generation (SSG) の実装
```getStaticProps``` という非同期関数を利用してAPIからデータを取得します。

```static-site-generation.tsx
import Head from "next/head";
import { title } from "process";
import { SAMPLE_API_ENDPOINT } from "./api";
import { RandomJoke } from "@/types";
import { FetchedRandomJoke } from "@/components/FetchedRandomJoke";

export const TITLE = "Static-Site Generation (SSG)";

export default function StaticSiteGeneration({
  randomJoke,
}: {
  randomJoke: RandomJoke;
}) {
  return (
    <>
      <Head>
        <title>{TITLE}</title>
      </Head>
      <h1>{TITLE}</h1>
      <p>
        Data fetched at <b>build-time</b> on the server-side before sending to
        the client.
      </p>
      <FetchedRandomJoke randomJoke={randomJoke} />
    </>
  );
}

// This function gets called at build time on server-side.
// It won't be called on client-side, so you can even do
// direct database queries.
export async function getStaticProps() {
  const response = await fetch(SAMPLE_API_ENDPOINT);
  const randomJoke = await response.json();

  return {
    props: {
      randomJoke, // will be passed to the page component as props
    },
  };
}
```
# Incremental Static Regeneration (ISR) の実装
SSG と同じく ```getStaticProps``` という非同期関数を利用してAPIからデータを取得します。ISR の場合、この関数に ```revalidate``` という props を追加します。

```incremental-static-regeneration.tsx
import Head from "next/head";
import { SAMPLE_API_ENDPOINT } from "./api";
import { RandomJoke } from "@/types";
import { FetchedRandomJoke } from "@/components/FetchedRandomJoke";

export const TITLE = "Incremental Static Regeneration (ISR)";

export default function IncrementalStaticRegeneration({
  randomJoke,
}: {
  randomJoke: RandomJoke;
}) {
  return (
    <>
      <Head>
        <title>{TITLE}</title>
      </Head>
      <h1>{TITLE}</h1>
      <p>
        Data fetched at build-time on the server-side and rebuilt when data
        updated.
      </p>
      <FetchedRandomJoke randomJoke={randomJoke} />
    </>
  );
}

// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// revalidation is enabled and a new request comes in
export async function getStaticProps() {
  const response = await fetch(SAMPLE_API_ENDPOINT);
  const randomJoke = await response.json();

  return {
    props: {
      randomJoke, // will be passed to the page component as props
    },

    // Next.js will attempt to re-generate the page:
    // - When a request comes in
    // - At most once every 5 seconds
    revalidate: 5, // In seconds
  };
}
```
# 最後に
4種類実装できたので、動作確認をします。

![localhost_3000-Google-Chrome-2023-07-09-15-59-58.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/b923259c-b77e-9b90-244e-ec99ccbf1dab.gif)


# 参考
- [Getting Started: Installation | Next.js](https://nextjs.org/docs/getting-started/installation)
- [Next.js: The Ultimate Cheat Sheet To Page Rendering](https://guydumais.digital/blog/next-js-the-ultimate-cheat-sheet-to-page-rendering/)
- [Rendering](https://nextjs.org/docs/pages/building-your-application/rendering)