nextjs-dynamic-routes-params by wsimmonds/claude-nextjs-skills
npx skills add https://github.com/wsimmonds/claude-nextjs-skills --skill nextjs-dynamic-routes-params在以下情况下使用此技能:
params 属性寻找将数据与 URL 路径关联的需求。
每当 UI 依赖于路径名的某一部分时,创建一个动态段([param])。典型信号包括:
/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;
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
❌ 静态页面响应
Implementation: app/page.tsx ← cannot access per-path identifiers
导致动态路由的示例需求
app/[id]/page.tsx 或 app/products/[id]/page.tsxapp/blog/[slug]/page.tsx 或 app/[slug]/page.tsxapp/docs/[...slug]/page.tsx核心规则: 如果数据随 URL 段变化,文件夹名称需要匹配的括号。
最常见错误: 向路由添加不必要的嵌套。
默认规则: 创建动态路由时,使用 app/[id]/page.tsx 或 app/[slug]/page.tsx,除非:
不要从资源名称推断嵌套:
app/[id]/page.tsx ✅(而不是 app/products/[id])app/[userId]/page.tsx ✅(而不是 app/users/[userId])app/[slug]/page.tsx ✅(而不是 app/blog/[slug])仅在明确告知时进行嵌套:
app/blog/[slug]/page.tsx ✅app/products/[id]/page.tsx ✅Next.js 使用 带方括号的文件夹名称 来创建动态路由段:
app/
├── [id]/page.tsx # 匹配 /123、/abc 等
├── blog/[slug]/page.tsx # 匹配 /blog/hello-world
├── shop/[category]/[id]/page.tsx # 匹配 /shop/electronics/123
└── docs/[...slug]/page.tsx # 匹配 /docs/a、/docs/a/b、/docs/a/b/c
关键原则: 文件夹结构就是路由结构。
关键规则:不要从资源类型名称推断路由结构!
仅仅因为您要获取“产品”或“用户”,并不意味着您需要 /products/[id] 或 /users/[id]。除非明确告知,否则优先选择最简单的结构。
决定路由结构时:
顶级动态路由(app/[id]/page.tsx)
/123,别名的 /abc-def嵌套动态路由(app/category/[id]/page.tsx)
/products/123、/blog/my-post(当指定时)多段动态路由(app/[cat]/[id]/page.tsx)
/shop/electronics/123⚠️ 常见错误: 当您应该创建 app/[id]/page.tsx 时,却创建了 app/products/[id]/page.tsx
❌ 错误: “通过 ID 获取产品”→ app/products/[id]/page.tsx
✅ 正确: “通过 ID 获取产品”→ app/[id]/page.tsx
❌ 错误: “为用户创建动态路由”→ app/users/[userId]/page.tsx
✅ 正确: “为用户创建动态路由”→ app/[userId]/page.tsx
仅在以下情况下添加类别前缀:
关键:在 Next.js 15+ 中,params 是一个 Promise,必须被等待!
// ✅ 正确 - 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>;
}
// ❌ 错误 - 将 params 视为同步对象(Next.js 15+)
export default async function ProductPage({
params,
}: {
params: { id: string }; // 缺少 Promise 包装器
}) {
const product = await fetch(`https://api.example.com/products/${params.id}`);
// 这将失败,因为 params 是一个 Promise!
}
对于 Next.js 14 及更早版本:
// Next.js 14 - params 是同步的
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>;
}
// 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;
// 使用 id...
}
// app/products/[id]/page.tsx(服务器组件)
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 }) {
// 使用 productId...
}
// app/[id]/page.tsx - 顶级动态路由
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>
);
}
// 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>
);
}
// 为 SSG 生成静态参数
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
// 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>
);
}
// app/docs/[...slug]/page.tsx - 匹配 /docs/a、/docs/a/b、/docs/a/b/c
interface PageProps {
params: Promise<{
slug: string[]; // 路径段数组
}>;
}
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 - 可选全捕获
// 匹配 /shop、/shop/electronics、/shop/electronics/phones
interface PageProps {
params: Promise<{
slug?: string[]; // 可选数组
}>;
}
export default async function ShopPage({ params }: PageProps) {
const { slug = [] } = await params;
if (slug.length === 0) {
return <ShopHomepage />;
}
return <CategoryPage category={slug.join('/')} />;
}
// 为可重用性单独定义 params 类型
type ProductPageParams = { id: string };
interface ProductPageProps {
params: Promise<ProductPageParams>;
}
export default async function ProductPage({ params }: ProductPageProps) {
const { id } = await params;
// id 被类型化为 string
}
// 在 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;
// 所有类型都正确
}
// ❌ 错误
export default async function Page({ params }: { params: { id: string } }) {
const product = await getProduct(params.id); // 错误:params 是 Promise
}
// ✅ 正确
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const product = await getProduct(id);
}
// ❌ 错误 - params 属性在客户端组件中不存在
'use client';
export default function ClientPage({ params }) { // undefined!
return <div>{params.id}</div>;
}
// ✅ 正确
'use client';
import { useParams } from 'next/navigation';
export default function ClientPage() {
const params = useParams<{ id: string }>();
return <div>{params.id}</div>;
}
// ❌ 不必要的嵌套
// app/products/product/[id]/page.tsx
// URL: /products/product/123
// ✅ 更简单
// app/products/[id]/page.tsx
// URL: /products/123
// 或者如果产品是主要资源,甚至更简单:
// app/[id]/page.tsx
// URL: /123
// ✅ 健壮
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(); // 显示 404 页面
}
return <div>{product.name}</div>;
}
当您需要创建动态路由时,请问:
URL 结构是什么?
[id]category/[id][category]/[id][...slug]这是服务器组件还是客户端组件?
params 属性(在 Next.js 15+ 中等待它)useParams() 钩子我需要最简单的结构吗?
我使用的是 Next.js 15+ 吗?
params 是 Promise<{...}>params 是 {...}// 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 }, // 每 60 秒重新验证
});
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('/'),
}));
}
在实现动态路由之前,请验证:
params 被类型化为 Promise<{...}>params(Next.js 15+)notFound())generateStaticParams() 用于 SSG| 场景 | 路由结构 | 参数访问 |
|---|---|---|
| 按 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() 钩子)提供对这些值的访问。
每周安装
112
仓库
GitHub 星标
80
首次出现
2026 年 1 月 23 日
安全审计
安装于
claude-code94
opencode89
codex87
gemini-cli84
cursor84
github-copilot84
Use this skill when:
params prop in page.tsx, layout.tsx, or route.tsLook for requirements that tie data to the URL path.
Create a dynamic segment ([param]) whenever the UI depends on part of the pathname. Typical signals include:
/products/{id}, /blog/{slug})/something/{identifier}✅ Dynamic route response
Requirement: display product information based on whichever ID appears in the URL
Implementation: app/[id]/page.tsx
Access parameter with: const { id } = await params;
❌ Static-page response
Implementation: app/page.tsx ← cannot access per-path identifiers
Example requirements that lead to dynamic routes
app/[id]/page.tsx or app/products/[id]/page.tsxapp/blog/[slug]/page.tsx or app/[slug]/page.tsxapp/docs/[...slug]/page.tsxCore rule: If data varies with a URL segment, the folder name needs matching brackets.
MOST COMMON MISTAKE: Adding unnecessary nesting to routes.
Default Rule: When creating a dynamic route, use app/[id]/page.tsx or app/[slug]/page.tsx unless:
Do NOT infer nesting from resource names:
app/[id]/page.tsx ✅ (not app/products/[id])app/[userId]/page.tsx ✅ (not app/users/[userId])app/[slug]/page.tsx ✅ (not app/blog/[slug])Only nest when explicitly told:
app/blog/[slug]/page.tsx ✅app/products/[id]/page.tsx ✅Next.js uses folder names with square brackets to create dynamic route segments:
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
Key Principle: The folder structure IS the route structure.
CRITICAL RULE: Do NOT infer route structure from resource type names!
Just because you're fetching a "product" or "user" doesn't mean you need /products/[id] or /users/[id]. Unless explicitly told otherwise, prefer the simplest structure.
When deciding on route structure:
Top-level dynamic route (app/[id]/page.tsx)
/123 for any resource, /abc-def for slugsNested dynamic route (app/category/[id]/page.tsx)
/products/123, /blog/my-post (when specified)⚠️ COMMON MISTAKE: Creating app/products/[id]/page.tsx when you should create app/[id]/page.tsx
❌ WRONG: "Fetch a product by ID" → app/products/[id]/page.tsx ✅ CORRECT: "Fetch a product by ID" → app/[id]/page.tsx
❌ WRONG: "Create a dynamic route for users" → app/users/[userId]/page.tsx ✅ CORRECT: "Create a dynamic route for users" → app/[userId]/page.tsx
Only add the category prefix when:
CRITICAL: In Next.js 15+,params is a Promise and must be awaited!
// ✅ 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!
}
For Next.js 14 and earlier:
// 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>;
}
// 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);
}
You CANNOT accessparams directly in Client Components. Instead:
useParams() hook:'use client';
import { useParams } from 'next/navigation';
export function ProductClient() {
const params = useParams<{ id: string }>();
const id = params.id;
// Use the id...
}
2. Pass params from Server Component:
// 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...
}
// 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>
);
}
// 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,
}));
}
// 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>
);
}
// 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('/')} />;
}
// 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
}
// ❌ 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);
}
// ❌ 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>;
}
// ❌ 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
// ✅ 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>;
}
When you need to create a dynamic route, ask:
What's the URL structure?
[id]category/[id][category]/[id][...slug]Is this a Server or Client Component?
params prop (await it in Next.js 15+)useParams() hookDo I need the simplest structure?
Am I on Next.js 15+?
// 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('/'),
}));
}
Before implementing a dynamic route, verify:
params is typed as Promise<{...}> for Next.js 15+params is awaited before accessing properties (Next.js 15+)notFound())generateStaticParams() for SSG if applicable| Scenario | Route Structure | Params Access |
|---|---|---|
| Single resource by ID | app/[id]/page.tsx | const { id } = await params |
| Category + resource | app/category/[id]/page.tsx | const { id } = await params |
| Blog with slugs | app/blog/[slug]/page.tsx | const { slug } = await params |
| Nested resources |
Remember: Dynamic routes in Next.js are file-system based. The folder structure with [brackets] creates the dynamic segments, and the params prop (or useParams() hook) provides access to those values.
Weekly Installs
112
Repository
GitHub Stars
80
First Seen
Jan 23, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
claude-code94
opencode89
codex87
gemini-cli84
cursor84
github-copilot84
Vue 3 调试指南:解决响应式、计算属性与监听器常见错误
11,800 周安装
AI代理协调框架:do by thedotmack/claude-mem - 多代理任务执行与验证系统
734 周安装
一键发布演示文稿到GitHub Pages:支持PPTX、PDF、HTML和Google Slides
738 周安装
oRPC 契约优先开发指南:TypeScript API 设计与 React Query 最佳实践
712 周安装
Rust性能优化指南:m10-performance技能详解,包含算法、数据结构与内存优化策略
715 周安装
Railway 模板部署指南:快速添加 Postgres、Redis、CMS 等服务
716 周安装
用户研究指南:方法、框架与交付成果全解析 | Anthropic知识工作插件
741 周安装
Multi-segment dynamic (app/[cat]/[id]/page.tsx)
/shop/electronics/123params is Promise<{...}>params is {...}app/[cat]/[id]/page.tsx |
const { cat, id } = await params |
| Flexible paths | app/docs/[...slug]/page.tsx | const { slug } = await params (slug is array) |
| Optional paths | app/[[...slug]]/page.tsx | const { slug = [] } = await params |
| Client Component | Use useParams() hook | const params = useParams<{ id: string }>() |