はじめに
Next.js では、2023年7月時点で以下2つのルーティング方法があります。
- App Router:Next.js 13から利用可能。現在はこちらが推奨。
- Pages Router: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)
※各レンダリング手法の仕組みやメリット・デメリット、ユースケースなどについては、別途こちらの記事でまとめています。
開発環境
開発環境は以下の通りです。
- Windows11
- VSCode
- Next.js 13.4.7
- React 18.2.0
- TypeScript 5.1.3
プロジェクト作成
まずは以下のコマンドを実行して、プロジェクトを作成します。
npx create-next-app@latest
インストール中にプロンプトが表示されるので、それぞれ以下の値を入力・選択します。
今回は Pages Router を使いたいので、Would you like to use App Router? (recommended)
は No
を選択します。それ以外は、デフォルト値通りにします。
インストールが完了するとプロジェクトファイルが作成されます。
以下のコマンドでローカルサーバーを起動し、http://localhost:3000 にアクセスするとアプリが表示されます。
npm run dev
実装するサンプルアプリ
以下のAPIから取得したデータを表示するアプリを作成します。
関連ページ・コンポーネントの追加
pages
フォルダ以下に各レンダリング手法の名前をつけたページを追加します。
次に 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">
->
</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">
->
</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">
->
</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">
->
</span>
</h2>
</Link>
</div>
</main>
);
}
最後にAPIから取得したデータを表示するコンポーネントを追加します。
このコンポーネントは全てのページで利用します。
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からデータを取得します。
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からデータを取得します。
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からデータを取得します。
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 を追加します。
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種類実装できたので、動作確認をします。