mcpField
N
nextjs-dynamic-routes-params

nextjs-dynamic-routes-params

github.com/wsimmonds開発109303 回インストール53 週間前に更新
リポジトリ

nextjs-dynamic-routes-params について

Next.js 開発者向けに、動的ルートと URL パラメーターの扱い方を解説するスキルです。サーバーとクライアントでの取得方法、Next.js 15+ の非同期 `params`、catch-all ルートを扱います。

主な機能

  1. 01動的ルートセグメントの作成
  2. 02サーバー側での params 取得
  3. 03useParams() によるクライアント側取得
  4. 04catch-all と optional catch-all ルート
  5. 05Next.js 15+ での params の await

活用シーン

  1. 01URL の識別子に応じた詳細ページ表示
  2. 02slug を使った記事ページの構築
  3. 03ネストしたドキュメントパスの実装
  4. 04過剰にネストしたルート構造の回避

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/wsimmonds/claude-nextjs-skills --skill nextjs-dynamic-routes-params

インストール後、Claude Code で/nextjs-dynamic-routes-paramsを入力して読み込まれたか確認します。

詳細

Next.js の動的ルートとパス名パラメーター

このスキルを使う場面

次の場合に、このスキルを使います。

  • 動的ルートセグメント(例:blog/[slug]、users/[id])を作成するとき
  • サーバーコンポーネントまたはクライアントコンポーネントで URL のパス名パラメーターにアクセスするとき
  • ルートパラメーターに基づいてデータを取得するページを構築するとき
  • キャッチオールルートまたはオプショナルキャッチオールルートを実装するとき
  • page.tsx、layout.tsx、または route.ts の params プロパティを扱うとき

⚠️ 動的ルートが必要な場面を見極める

データが URL パスに紐づく要件がないか確認してください。

UI がパス名の一部に依存する場合は、必ず動的セグメント([param])を作成します。よくある兆候は次のとおりです。

  • 「URL にあるアイテムの ID/slug」を参照する詳細ページ
  • パスセグメントを明示する文言(例:/products/{id}、/blog/{slug})
  • 「アクセスしているリソースに応じて」データを取得する要件
  • あるページから /something/{identifier} に移動するナビゲーションフロー

✅ 動的ルートの応答

Requirement: display product information based on whichever ID appears in the URL
Implementation: app/[id]/page.tsx
Access parameter with: const { id } = await params;

❌ 静的ページの応答

Implementation: app/page.tsx  ← cannot access per-path identifiers

動的ルートにつながる要件の例

  1. 「URL に表示された商品 ID に応じて読み込む商品ページを表示する」→ app/[id]/page.tsx または app/products/[id]/page.tsx
  2. 「slug に基づいてブログ記事を表示する」→ app/blog/[slug]/page.tsx または app/[slug]/page.tsx
  3. 「/docs/getting-started/installation のようなネストされたドキュメントをサポートする」→ app/docs/[...slug]/page.tsx

基本ルール: URL セグメントによってデータが変わる場合、フォルダー名には対応する角括弧が必要です。

⚠️ 重要:ルート構造を過度に複雑にしない

最もよくある間違い: 不要なルートのネストを追加することです。

デフォルトのルール: 動的ルートを作成するときは、次の場合を除き、app/[id]/page.tsx または app/[slug]/page.tsx を使用します。

  • URL 構造が明示的に指定されている(例:「/products/[id] にルートを作成する」)
  • 名前空間が必要な複数のリソースタイプを構築している
  • 要件でネストされた URL 構造が明確に示されている

リソース名からネスト構造を推測しないでください。

  • 「ID で商品を取得する」→ app/[id]/page.tsx ✅(app/products/[id] ではありません)
  • 「ユーザープロフィールを表示する」→ app/[userId]/page.tsx ✅(app/users/[userId] ではありません)
  • 「ブログ記事を表示する」→ app/[slug]/page.tsx ✅(app/blog/[slug] ではありません)

明示的に指示された場合にのみネストします。

  • 「/blog/[slug] にルートを作成する」→ app/blog/[slug]/page.tsx ✅
  • 「商品は /products/[id] に配置する」→ app/products/[id]/page.tsx ✅

基本概念

動的ルートの構文

Next.js では、角括弧で囲んだフォルダー名を使って動的ルートセグメントを作成します。

app/
├── [id]/page.tsx              # Matches /123, /abc, etc.
├── blog/[slug]/page.tsx       # Matches /blog/hello-world
├── shop/[category]/[id]/page.tsx  # Matches /shop/electronics/123
└── docs/[...slug]/page.tsx    # Matches /docs/a, /docs/a/b, /docs/a/b/c

重要な原則: フォルダー構造がそのままルート構造になります。

ルート構造の判断ツリー

重要なルール:リソースの種類の名前からルート構造を推測しないでください!

「product」や「user」を取得するからといって、/products/[id] や /users/[id] が必要とは限りません。明示的な指示がない限り、最もシンプルな構造を優先してください。

ルート構造を決める際は、次の点を確認してください:

  1. トップレベルの動的ルート(app/[id]/page.tsx)

    • デフォルトの選択肢 — 特に指示がない限り、これを使用します
    • リソースが主要なエンティティである場合に使用します
    • ID ベースのルーティングだけが必要な場合に使用します
    • 例:任意のリソースを表す /123、スラッグを表す /abc-def
    • パターン:必要な識別子は ID/スラッグのみです
    • 迷ったら、こちらを選んでください!
  2. ネストされた動的ルート(app/category/[id]/page.tsx)

    • URL 構造で明示的に必要な場合にのみ使用します
    • 「/products/[id] ルートを作成して」と指示された場合に使用します
    • URL 自体にカテゴリのプレフィックスが必要な場合に使用します
    • 例:/products/123、/blog/my-post(指定された場合)
    • パターン:カテゴリと識別子の両方が必要です
  3. 複数セグメントの動的ルート(app/[cat]/[id]/page.tsx)

    • 階層が重要な場合に使用します
    • 例:/shop/electronics/123
    • パターン:複数階層の分類

⚠️ よくある間違い: app/[id]/page.tsx を作成すべきところで、app/products/[id]/page.tsx を作成する

❌ 誤り: 「ID で product を取得する」→ app/products/[id]/page.tsx ✅ 正解: 「ID で product を取得する」→ app/[id]/page.tsx

❌ 誤り: 「user 用の動的ルートを作成する」→ app/users/[userId]/page.tsx ✅ 正解: 「user 用の動的ルートを作成する」→ app/[userId]/page.tsx

カテゴリのプレフィックスを追加するのは、次の場合に限ります:

  • 要件に「/products/... で」などと明記されている
  • 名前空間を分ける必要のある複数のリソースタイプを構築している
  • 要件で URL 構造が指定されている

パス名パラメーターへのアクセス

サーバーコンポーネントの場合(page.tsx、layout.tsx)

重要:Next.js 15 以降では、params は Promise であり、await する必要があります!

// ✅ CORRECT - Next.js 15+
export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  const product = await fetch(`https://api.example.com/products/${id}`)
    .then(res => res.json());

  return <div>{product.name}</div>;
}
// ❌ WRONG - Treating params as synchronous object (Next.js 15+)
export default async function ProductPage({
  params,
}: {
  params: { id: string };  // Missing Promise wrapper
}) {
  const product = await fetch(`https://api.example.com/products/${params.id}`);
  // This will fail because params is a Promise!
}

Next.js 14 以前の場合:

// Next.js 14 - params is synchronous
export default async function ProductPage({
  params,
}: {
  params: { id: string };
}) {
  const product = await fetch(`https://api.example.com/products/${params.id}`)
    .then(res => res.json());

  return <div>{product.name}</div>;
}
ルートハンドラーの場合(route.ts)
// app/api/products/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;

  const product = await db.products.findById(id);
  return Response.json(product);
}
クライアントコンポーネントの場合

クライアントコンポーネントでは params に直接アクセスできません。 代わりに、次の方法を使います:

  1. useParams() フックを使用する:
'use client';

import { useParams } from 'next/navigation';

export function ProductClient() {
  const params = useParams<{ id: string }>();
  const id = params.id;

  // Use the id...
}
  1. サーバーコンポーネントから params を渡す:
// app/products/[id]/page.tsx (Server Component)
export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  return <ProductClient productId={id} />;
}

// components/ProductClient.tsx
'use client';

export function ProductClient({ productId }: { productId: string }) {
  // Use productId...
}

よく使われるパターン

パターン 1:シンプルな ID ベースのページ
// app/[id]/page.tsx - Top-level dynamic route
interface PageProps {
  params: Promise<{ id: string }>;
}

export default async function ItemPage({ params }: PageProps) {
  const { id } = await params;

  const item = await fetch(`https://api.example.com/items/${id}`)
    .then(res => res.json());

  return (
    <div>
      <h1>{item.title}</h1>
      <p>{item.description}</p>
    </div>
  );
}
パターン 2:スラッグを使ったブログ記事
// app/blog/[slug]/page.tsx
interface PageProps {
  params: Promise<{ slug: string }>;
}

export default async function BlogPost({ params }: PageProps) {
  const { slug } = await params;

  const post = await getPostBySlug(slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

// Generate static params for SSG
export async function generateStaticParams() {
  const posts = await getAllPosts();

  return posts.map((post) => ({
    slug: post.slug,
  }));
}
パターン 3:ネストされたリソース
// app/users/[userId]/posts/[postId]/page.tsx
interface PageProps {
  params: Promise<{
    userId: string;
    postId: string;
  }>;
}

export default async function UserPost({ params }: PageProps) {
  const { userId, postId } = await params;

  const [user, post] = await Promise.all([
    getUserById(userId),
    getPostById(postId),
  ]);

  return (
    <div>
      <h1>{post.title}</h1>
      <p>By {user.name}</p>
      <div>{post.content}</div>
    </div>
  );
}
パターン 4:キャッチオールルート
// app/docs/[...slug]/page.tsx - Matches /docs/a, /docs/a/b, /docs/a/b/c
interface PageProps {
  params: Promise<{
    slug: string[];  // Array of path segments
  }>;
}

export default async function DocsPage({ params }: PageProps) {
  const { slug } = await params;
  const path = slug.join('/');

  const doc = await getDocByPath(path);

  return <div>{doc.content}</div>;
}
// app/shop/[[...slug]]/page.tsx - Optional catch-all
// Matches /shop, /shop/electronics, /shop/electronics/phones
interface PageProps {
  params: Promise<{
    slug?: string[];  // Optional array
  }>;
}

export default async function ShopPage({ params }: PageProps) {
  const { slug = [] } = await params;

  if (slug.length === 0) {
    return <ShopHomepage />;
  }

  return <CategoryPage category={slug.join('/')} />;
}

TypeScript のベストプラクティス

params の型安全性
// Define params type separately for reusability
type ProductPageParams = { id: string };

interface ProductPageProps {
  params: Promise<ProductPageParams>;
}

export default async function ProductPage({ params }: ProductPageProps) {
  const { id } = await params;
  // id is typed as string
}

// Reuse in generateMetadata
export async function generateMetadata({ params }: ProductPageProps) {
  const { id } = await params;
  const product = await getProduct(id);
  return { title: product.name };
}
複数の動的セグメント
type PostPageParams = {
  category: string;
  slug: string;
};

interface PostPageProps {
  params: Promise<PostPageParams>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

export default async function PostPage({ params, searchParams }: PostPageProps) {
  const { category, slug } = await params;
  const { view } = await searchParams;

  // All properly typed
}

よくある落とし穴と解決策

落とし穴 1:params が Promise であることを忘れる(Next.js 15 以降)
// ❌ WRONG
export default async function Page({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);  // Error: params is Promise
}

// ✅ CORRECT
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const product = await getProduct(id);
}
落とし穴 2:useParams を使わずにクライアントコンポーネントで params を使用する
// ❌ WRONG - params prop doesn't exist in Client Components
'use client';

export default function ClientPage({ params }) {  // undefined!
  return <div>{params.id}</div>;
}

// ✅ CORRECT
'use client';
import { useParams } from 'next/navigation';

export default function ClientPage() {
  const params = useParams<{ id: string }>();
  return <div>{params.id}</div>;
}
落とし穴 3:ルートを過度にネストする
// ❌ UNNECESSARY NESTING
// app/products/product/[id]/page.tsx
// URL: /products/product/123

// ✅ SIMPLER
// app/products/[id]/page.tsx
// URL: /products/123

// OR even simpler if product is the main resource:
// app/[id]/page.tsx
// URL: /123
落とし穴 4:無効な ID を処理しない
// ✅ ROBUST
export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  const product = await fetch(`https://api.example.com/products/${id}`)
    .then(res => {
      if (!res.ok) throw new Error('Product not found');
      return res.json();
    });

  if (!product) {
    notFound();  // Shows 404 page
  }

  return <div>{product.name}</div>;
}

判断ガイド

動的ルートを作成する必要がある場合は、次の点を確認してください:

  1. URL 構造はどうなっていますか?

    • ID のみ:[id]
    • カテゴリと ID:category/[id]
    • 階層構造:[category]/[id]
    • 柔軟なパス:[...slug]
  2. サーバーコンポーネントですか、それともクライアントコンポーネントですか?

    • サーバー:params プロパティを使用します(Next.js 15 以降では await します)
    • クライアント:useParams() フックを使用します
  3. 最もシンプルな構造が必要ですか?

    • 迷ったら、ネストの階層を少なくします
    • トップレベルのルートは、よりシンプルで直接的です
  4. Next.js 15 以降を使用していますか?

    • はい:params は Promise<{...}> です
    • いいえ:params は {...} です

例:実際の使用シナリオ

E コマースの商品ページ
// app/products/[id]/page.tsx
import { notFound } from 'next/navigation';

interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
}

async function getProduct(id: string): Promise<Product | null> {
  const res = await fetch(`https://api.example.com/products/${id}`, {
    next: { revalidate: 60 },  // Revalidate every 60s
  });

  if (!res.ok) return null;
  return res.json();
}

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const product = await getProduct(id);

  if (!product) {
    notFound();
  }

  return (
    <div>
      <h1>{product.name}</h1>
      <p>${product.price}</p>
      <p>{product.description}</p>
    </div>
  );
}

export async function generateMetadata({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const product = await getProduct(id);

  return {
    title: product?.name ?? 'Product Not Found',
    description: product?.description,
  };
}
ネストされたパスを持つドキュメントサイト
// app/docs/[...slug]/page.tsx
interface DocPageProps {
  params: Promise<{ slug: string[] }>;
}

export default async function DocPage({ params }: DocPageProps) {
  const { slug } = await params;
  const path = slug.join('/');

  const doc = await getDocByPath(path);

  if (!doc) {
    notFound();
  }

  return (
    <article className="prose">
      <h1>{doc.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: doc.html }} />
    </article>
  );
}

export async function generateStaticParams() {
  const docs = await getAllDocs();

  return docs.map((doc) => ({
    slug: doc.path.split('/'),
  }));
}

動的ルートのチェックリスト

動的ルートを実装する前に、次の点を確認してください。

  • ルート構造はできるだけシンプルであること(特に指示がなければトップレベルにします)
  • リソースの種類名からルートのネストを推測していないこと
  • ルート構造が URL の要件に合っていること(過度にネストされていないこと)
  • params が Next.js 15 以降では Promise<{...}> として型付けされていること
  • プロパティにアクセスする前に params を await していること(Next.js 15 以降)
  • 無効な ID/スラッグに対するエラー処理があること(notFound() を使用)
  • TypeScript の型が適切に定義されていること
  • クライアント側のインタラクティブ性が必要でない限り、サーバーコンポーネントを使用していること
  • 該当する場合、SSG に generateStaticParams() を検討していること
  • SEO が重要な場合、メタデータを生成していること

クイックリファレンス

シナリオルート構造Params のアクセス方法
ID による単一リソースapp/[id]/page.tsxconst { id } = await params
カテゴリ+リソースapp/category/[id]/page.tsxconst { id } = await params
スラッグを使うブログapp/blog/[slug]/page.tsxconst { slug } = await params
ネストされたリソースapp/[cat]/[id]/page.tsxconst { cat, id } = await params
柔軟なパスapp/docs/[...slug]/page.tsxconst { slug } = await params(slug は配列です)
オプションのパスapp/[[...slug]]/page.tsxconst { slug = [] } = await params
クライアントコンポーネントuseParams() フックを使用const params = useParams<{ id: string }>()

覚えておいてください: Next.js の動的ルートはファイルシステムに基づいています。[brackets] を含むフォルダー構造によって動的セグメントが作成され、params プロパティ(または useParams() フック)からそれらの値にアクセスできます。

コメント