動的ルート(Dynamic Route)の作り方まとめ
はじめに
よく作品を作っている時に/[id]/page.tsxのような実装方法を忘れてしまうので、
今回はメモ用に残しておきます。
/[id]/page.tsx は何か?
/[id]/page.tsx のように角かっこでパスを囲むフォルダは、
Next.js の 動的ルート(Dynamic Route) 機能です。
例えば、/1 にアクセス → /[id]/page.tsx が呼ばれ、params.id に '1' が渡される。
みたいな実装で、主に記事などの詳細ページで使うのかなと思っています。
今回は簡易的な写真日記のようなものを作成します。
モックデータを用意して、それを元に動的ルート機能を実装してみようと思います。
今回の構成
/app
/lib
mockData.ts
/[id]
page.tsx
page.tsx
/public
image_001.jpg
image_002.jpg
image_003.jpg
image_004.jpg
mockData.tsを作成
まず初めに、mockData.tsを作成します。
/app/lib/mockData.ts
export type Item = {
id: string;
image: string;
text: string;
};
export const MOCK_TASKS: Item[] = [
{ id: '1', image: '/image_001.jpg', text: '山に登りました' },
{ id: '2', image: '/image_002.jpg', text: '旅行に行ってきました' },
{ id: '3', image: '/image_003.jpg', text: 'この日はお気に入りのカフェに行った' },
{ id: '4', image: '/image_004.jpg', text: 'ラーメンを食べました' },
];
一応TOPページも作る
今回は簡単ではあるが、詳細ページへの導線としてTOPも作ります。
TOPページではMOCK_TASKSをmapで回して、それぞれの詳細ページ(/[id])にリンクさせます。
/app/page.tsx
import Image from 'next/image';
import Link from 'next/link';
import { MOCK_TASKS } from './lib/mockData';
export default function GalleryList() {
return (
<div className="w-[90%] mx-auto grid gap-6 md:grid-cols-2">
{MOCK_TASKS.map((item) => (
<div key={item.id}>
<Link href={`/${item.id}`}>
<div className="w-full h-[300px]">
<Image
src={item.image}
alt={item.text}
width={300}
height={300}
className="w-full h-full object-cover"
/>
</div>
<p className="text-gray-500 text-center mt-2">{item.text}</p>
</Link>
</div>
))}
</div>
);
}
本題の詳細ページ
下記が本題の詳細ページになります。
/app/[id]/page.tsx
import Image from 'next/image';
import { notFound } from 'next/navigation';
import { use } from 'react';
import { MOCK_TASKS } from '../lib/mockData';
export type GalleryItemDetailProps = {
params: Promise<{
id: string;
}>;
};
export default function GalleryItemDetail({ params }: GalleryItemDetailProps) {
const resolvedParams = use(params); // ← Promise を解決
const itemId = Number(resolvedParams.id);
const item = MOCK_TASKS.find((task) => Number(task.id) === itemId);
if (!item) {
notFound(); // ← 見つからない場合は 404 を返す
}
return (
<div className="w-[90%] mx-auto">
<h1 className="text-3xl font-bold underline mb-4">ギャラリー詳細</h1>
<div className="md:flex items-start gap-6">
<div>
<Image
src={`${item.image}`}
alt={item.text}
width={300}
height={300}
className="w-full h-full"
/>
</div>
<div>
<p>{item.text}</p>
</div>
</div>
</div>
);
}
// 静的生成 (SSG)
export async function generateStaticParams() {
return MOCK_TASKS.map((task) => ({
id: String(task.id),
}));
}
解説
気になる点を解説します。
notFound() の役割
・import { notFound } from 'next/navigation'
・データが存在しないときに Next.js 標準の404ページを表示する。
・もし app/not-found.tsx を用意していれば、カスタム404ページを表示できます。
if (!item) {
notFound();
}
use() の役割(Next.js 15での重要ポイント)
Next.js 15以降、動的ルートパラメータ(params)とクエリパラメータ(searchParams)は同期的なオブジェクトではなく、Promiseとして渡されます。
Next.js15でparamsがPromiseとして渡されるようになった背景をGeminiに聞いてみたところ、
App Routerにおけるストリーミングとパフォーマンスの最適化をさらに推し進めるため
と回答をいただきました。
use()フックは、Promiseを受け取って、そのPromiseが解決するまで待機し、中身の値を同期的に返す役割を果たします。
const resolvedParams = use(params); // ← Promise を解決
const itemId = Number(resolvedParams.id);
型定義の注意点
以前の Next.js 14 では
params: { id: string }
でOKでしたが、
Next.js 15では下記のようにしないと型エラーになります。
params: Promise<{ id: string }>
静的生成 (SSG)とは?
動的ルート([id] など)を使うと、Next.jsはどのidのページを作ればいいのかが分かりません。
そこで、この関数でビルド時に生成しておきたいパラメータを返します。
export async function generateStaticParams() {
return MOCK_TASKS.map((task) => ({
id: String(task.id),
}));
}
この関数が返す配列の要素(ここでは id)ごとに、Next.js が静的ページを作成します。
例えば上記の場合、/1, /2, /3, /4の4ページがビルド時に生成されます。
まとめ
-
/[id]/page.tsxは 動的ルート のための構文。 - Next.js 15から
paramsがPromiseになったため、use()で解決する。
→型定義にも注意 -
notFound()は存在チェックと404制御に使う。 -
generateStaticParams()で SSGに対応。
とりあえずこれで動的ルート(Dynamic Route)は問題がなさそう!
Discussion