🫠

動的ルート(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ページがビルド時に生成されます。

まとめ

  1. /[id]/page.tsxは 動的ルート のための構文。
  2. Next.js 15からparamsPromiseになったため、use()で解決する。
     →型定義にも注意
  3. notFound()は存在チェックと404制御に使う。
  4. generateStaticParams()で SSGに対応。

とりあえずこれで動的ルート(Dynamic Route)は問題がなさそう!

Discussion