nextjs-dynamic-routes-params について
Next.js 開発者向けに、動的ルートと URL パラメーターの扱い方を解説するスキルです。サーバーとクライアントでの取得方法、Next.js 15+ の非同期 `params`、catch-all ルートを扱います。
主な機能
- 01動的ルートセグメントの作成
- 02サーバー側での params 取得
- 03useParams() によるクライアント側取得
- 04catch-all と optional catch-all ルート
- 05Next.js 15+ での params の await
活用シーン
- 01URL の識別子に応じた詳細ページ表示
- 02slug を使った記事ページの構築
- 03ネストしたドキュメントパスの実装
- 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
動的ルートにつながる要件の例
- 「URL に表示された商品 ID に応じて読み込む商品ページを表示する」→
app/[id]/page.tsxまたはapp/products/[id]/page.tsx - 「slug に基づいてブログ記事を表示する」→
app/blog/[slug]/page.tsxまたはapp/[slug]/page.tsx - 「/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] が必要とは限りません。明示的な指示がない限り、最もシンプルな構造を優先してください。
ルート構造を決める際は、次の点を確認してください:
-
トップレベルの動的ルート(
app/[id]/page.tsx)- デフォルトの選択肢 — 特に指示がない限り、これを使用します
- リソースが主要なエンティティである場合に使用します
- ID ベースのルーティングだけが必要な場合に使用します
- 例:任意のリソースを表す
/123、スラッグを表す/abc-def - パターン:必要な識別子は ID/スラッグのみです
- 迷ったら、こちらを選んでください!
-
ネストされた動的ルート(
app/category/[id]/page.tsx)- URL 構造で明示的に必要な場合にのみ使用します
- 「
/products/[id]ルートを作成して」と指示された場合に使用します - URL 自体にカテゴリのプレフィックスが必要な場合に使用します
- 例:
/products/123、/blog/my-post(指定された場合) - パターン:カテゴリと識別子の両方が必要です
-
複数セグメントの動的ルート(
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 に直接アクセスできません。 代わりに、次の方法を使います:
useParams()フックを使用する:
'use client';
import { useParams } from 'next/navigation';
export function ProductClient() {
const params = useParams<{ id: string }>();
const id = params.id;
// Use the id...
}
- サーバーコンポーネントから 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>;
}
判断ガイド
動的ルートを作成する必要がある場合は、次の点を確認してください:
-
URL 構造はどうなっていますか?
- ID のみ:
[id] - カテゴリと ID:
category/[id] - 階層構造:
[category]/[id] - 柔軟なパス:
[...slug]
- ID のみ:
-
サーバーコンポーネントですか、それともクライアントコンポーネントですか?
- サーバー:
paramsプロパティを使用します(Next.js 15 以降では await します) - クライアント:
useParams()フックを使用します
- サーバー:
-
最もシンプルな構造が必要ですか?
- 迷ったら、ネストの階層を少なくします
- トップレベルのルートは、よりシンプルで直接的です
-
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.tsx | const { id } = await params |
| カテゴリ+リソース | app/category/[id]/page.tsx | const { id } = await params |
| スラッグを使うブログ | app/blog/[slug]/page.tsx | const { slug } = await params |
| ネストされたリソース | app/[cat]/[id]/page.tsx | const { cat, id } = await params |
| 柔軟なパス | app/docs/[...slug]/page.tsx | const { slug } = await params(slug は配列です) |
| オプションのパス | app/[[...slug]]/page.tsx | const { slug = [] } = await params |
| クライアントコンポーネント | useParams() フックを使用 | const params = useParams<{ id: string }>() |
覚えておいてください: Next.js の動的ルートはファイルシステムに基づいています。[brackets] を含むフォルダー構造によって動的セグメントが作成され、params プロパティ(または useParams() フック)からそれらの値にアクセスできます。