nextjs-dynamic-routes-params
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNext.js Dynamic Routes and Pathname Parameters
Next.js 动态路由与路径参数
When to Use This Skill
何时使用此技能
Use this skill when:
- Creating dynamic route segments (e.g., blog/[slug], users/[id])
- Accessing URL pathname parameters in Server or Client Components
- Building pages that fetch data based on route parameters
- Implementing catch-all or optional catch-all routes
- Working with the prop in page.tsx, layout.tsx, or route.ts
params
在以下场景中使用本技能:
- 创建动态路由片段(例如:blog/[slug]、users/[id])
- 在Server或Client组件中访问URL路径参数
- 构建基于路由参数获取数据的页面
- 实现捕获所有或可选捕获所有路由
- 在page.tsx、layout.tsx或route.ts中使用属性
params
⚠️ RECOGNIZING WHEN YOU NEED DYNAMIC ROUTES
⚠️ 识别何时需要动态路由
Look for requirements that tie data to the URL path.
Create a dynamic segment () whenever the UI depends on part of the pathname. Typical signals include:
[param]- Details pages that reference “the item’s ID/slug from the URL”
- Copy that calls out path segments (e.g., ,
/products/{id})/blog/{slug} - Requirements to fetch data “based on whichever resource is being visited”
- Navigation flows where one page links to
/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 identifiersExample requirements that lead to dynamic routes
- “Show a product page that loads whichever product ID appears in the URL” → or
app/[id]/page.tsxapp/products/[id]/page.tsx - “Render a blog article based on its slug” → or
app/blog/[slug]/page.tsxapp/[slug]/page.tsx - “Support nested docs such as /docs/getting-started/installation” →
app/docs/[...slug]/page.tsx
Core rule: If data varies with a URL segment, the folder name needs matching brackets.
寻找将数据与URL路径关联的需求。
每当UI依赖路径名的某一部分时,就创建一个动态片段()。典型信号包括:
[param]- 详情页面需要“从URL中获取项目的ID/别名”
- 文案中明确提及路径片段(例如:、
/products/{id})/blog/{slug} - 需求要求“根据当前访问的资源获取数据”
- 导航流程中存在跳转到的页面链接
/something/{identifier}
✅ 动态路由实现示例
需求:根据URL中的ID显示对应产品信息
实现:app/[id]/page.tsx
参数获取方式:const { id } = await params;❌ 静态页面实现(错误示例)
实现:app/page.tsx ← 无法获取基于路径的标识符会导向动态路由的需求示例
- “展示产品页面,加载URL中对应ID的产品” → 或
app/[id]/page.tsxapp/products/[id]/page.tsx - “根据别名渲染博客文章” → 或
app/blog/[slug]/page.tsxapp/[slug]/page.tsx - “支持嵌套文档,例如/docs/getting-started/installation” →
app/docs/[...slug]/page.tsx
核心规则: 如果数据随URL片段变化,文件夹名称需要使用对应的方括号。
⚠️ CRITICAL: Avoid Over-Engineering Route Structure
⚠️ 重要提示:避免过度设计路由结构
MOST COMMON MISTAKE: Adding unnecessary nesting to routes.
Default Rule: When creating a dynamic route, use or unless:
app/[id]/page.tsxapp/[slug]/page.tsx- The URL structure is explicitly specified (e.g., "create route at /products/[id]")
- You're building multiple resource types that need namespacing
- The requirements clearly show a nested URL structure
Do NOT infer nesting from resource names:
- "Fetch a product by ID" → ✅ (not
app/[id]/page.tsx)app/products/[id] - "Show user profile" → ✅ (not
app/[userId]/page.tsx)app/users/[userId] - "Display blog post" → ✅ (not
app/[slug]/page.tsx)app/blog/[slug]
Only nest when explicitly told:
- "Create a route at /blog/[slug]" → ✅
app/blog/[slug]/page.tsx - "Products should be at /products/[id]" → ✅
app/products/[id]/page.tsx
最常见错误: 给路由添加不必要的嵌套层级。
默认规则: 创建动态路由时,使用或,除非满足以下条件:
app/[id]/page.tsxapp/[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
Core Concepts
核心概念
Dynamic Route Syntax
动态路由语法
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/cKey Principle: The folder structure IS the route structure.
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关键原则: 文件夹结构就是路由结构。
Route Structure Decision Tree
路由结构决策树
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 or . Unless explicitly told otherwise, prefer the simplest structure.
/products/[id]/users/[id]When deciding on route structure:
-
Top-level dynamic route ()
app/[id]/page.tsx- DEFAULT CHOICE - Use this unless specifically told otherwise
- Use when the resource IS the primary entity
- Use when only ID-based routing is needed
- Examples: for any resource,
/123for slugs/abc-def - Pattern: The ID/slug is the only identifier needed
- When in doubt, choose this!
-
Nested dynamic route ()
app/category/[id]/page.tsx- ONLY use when explicitly required by the URL structure
- Use when you're told "create a /products/[id] route"
- Use when the URL itself needs the category prefix
- Examples: ,
/products/123(when specified)/blog/my-post - Pattern: Category + identifier (when both are required)
-
Multi-segment dynamic ()
app/[cat]/[id]/page.tsx- Use when hierarchy matters
- Examples:
/shop/electronics/123 - Pattern: Multiple levels of categorization
⚠️ COMMON MISTAKE: Creating when you should create
app/products/[id]/page.tsxapp/[id]/page.tsx❌ WRONG: "Fetch a product by ID" →
✅ CORRECT: "Fetch a product by ID" →
app/products/[id]/page.tsxapp/[id]/page.tsx❌ WRONG: "Create a dynamic route for users" →
✅ CORRECT: "Create a dynamic route for users" →
app/users/[userId]/page.tsxapp/[userId]/page.tsxOnly add the category prefix when:
- The requirement explicitly says "at /products/..." or similar
- You're building multiple resource types that need namespacing
- The URL structure is specified in requirements
重要规则:不要从资源类型名称推断路由结构!
仅仅因为你要获取“产品”或“用户”数据,并不意味着需要或。除非有明确要求,否则优先选择最简结构。
/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.tsxapp/products/[id]/page.tsx❌ 错误: “根据ID获取产品” →
✅ 正确: “根据ID获取产品” →
app/products/[id]/page.tsxapp/[id]/page.tsx❌ 错误: “为用户创建动态路由” →
✅ 正确: “为用户创建动态路由” →
app/users/[userId]/page.tsxapp/[userId]/page.tsx仅在以下情况添加分类前缀:
- 需求中明确说明“路径为/products/...”或类似表述
- 你正在构建多种需要命名空间区分的资源类型
- URL结构在需求中有明确规定
Accessing Pathname Parameters
访问路径参数
In Server Components (page.tsx, layout.tsx)
在Server组件中(page.tsx、layout.tsx)
CRITICAL: In Next.js 15+, is a Promise and must be awaited!
paramstypescript
// ✅ 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>;
}typescript
// ❌ 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:
typescript
// 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>;
}重要提示:在Next.js 15+中,是一个Promise,必须使用await!
paramstypescript
// ✅ 正确写法 - 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>;
}typescript
// ❌ 错误写法 - 将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及更早版本:
typescript
// 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>;
}In Route Handlers (route.ts)
在路由处理器中(route.ts)
typescript
// 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);
}typescript
// 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);
}In Client Components
在Client组件中
You CANNOT access directly in Client Components. Instead:
params- Use hook:
useParams()
typescript
'use client';
import { useParams } from 'next/navigation';
export function ProductClient() {
const params = useParams<{ id: string }>();
const id = params.id;
// Use the id...
}- Pass params from Server Component:
typescript
// 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...
}你不能在Client组件中直接访问。替代方案:
params- 使用钩子:
useParams()
typescript
'use client';
import { useParams } from 'next/navigation';
export function ProductClient() {
const params = useParams<{ id: string }>();
const id = params.id;
// 使用id...
}- 从Server组件传递params:
typescript
// app/products/[id]/page.tsx(Server组件)
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...
}Common Patterns
常见模式
Pattern 1: Simple ID-Based Page
模式1:简单的基于ID的页面
typescript
// 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>
);
}typescript
// 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>
);
}Pattern 2: Blog Post with Slug
模式2:带别名的博客文章
typescript
// 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,
}));
}typescript
// 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,
}));
}Pattern 3: Nested Resources
模式3:嵌套资源
typescript
// 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>
);
}typescript
// 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>作者:{user.name}</p>
<div>{post.content}</div>
</div>
);
}Pattern 4: Catch-All Routes
模式4:捕获所有路由
typescript
// 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>;
}typescript
// 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
// 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>;
}typescript
// 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('/')} />;
}TypeScript Best Practices
TypeScript最佳实践
Type Safety for Params
Params的类型安全
typescript
// 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 };
}typescript
// 单独定义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 };
}Multiple Dynamic Segments
多动态片段
typescript
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
}typescript
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;
// 所有参数都有正确的类型
}Common Pitfalls and Solutions
常见陷阱与解决方案
Pitfall 1: Forgetting params is a Promise (Next.js 15+)
陷阱1:忘记在Next.js 15+中params是Promise
typescript
// ❌ 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);
}typescript
// ❌ 错误写法
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);
}Pitfall 2: Using params in Client Components without useParams
陷阱2:在Client组件中不使用useParams直接访问params
typescript
// ❌ 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>;
}typescript
// ❌ 错误写法 - Client组件中不存在params属性
'use client';
export default function ClientPage({ params }) { // 未定义!
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>;
}Pitfall 3: Over-nesting Routes
陷阱3:过度嵌套路由
typescript
// ❌ 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: /123typescript
// ❌ 不必要的嵌套
// app/products/product/[id]/page.tsx
// URL: /products/product/123
// ✅ 更简洁的写法
// app/products/[id]/page.tsx
// URL: /products/123
// 如果产品是主要资源,甚至可以更简洁:
// app/[id]/page.tsx
// URL: /123Pitfall 4: Not Handling Invalid IDs
陷阱4:未处理无效ID
typescript
// ✅ 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>;
}typescript
// ✅ 健壮的写法
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('产品不存在');
return res.json();
});
if (!product) {
notFound(); // 显示404页面
}
return <div>{product.name}</div>;
}Decision Guide
决策指南
When you need to create a dynamic route, ask:
-
What's the URL structure?
- Single ID:
[id] - Category + ID:
category/[id] - Hierarchical:
[category]/[id] - Flexible paths:
[...slug]
- Single ID:
-
Is this a Server or Client Component?
- Server: Use prop (await it in Next.js 15+)
params - Client: Use hook
useParams()
- Server: Use
-
Do I need the simplest structure?
- When in doubt, use fewer nesting levels
- Top-level routes are simpler and more direct
-
Am I on Next.js 15+?
- Yes: is
paramsPromise<{...}> - No: is
params{...}
- Yes:
当你需要创建动态路由时,问自己以下问题:
-
URL结构是什么?
- 单个ID:
[id] - 分类+ID:
category/[id] - 层级结构:
[category]/[id] - 灵活路径:
[...slug]
- 单个ID:
-
这是Server组件还是Client组件?
- Server组件:使用属性(Next.js 15+中需await)
params - Client组件:使用钩子
useParams()
- Server组件:使用
-
我需要最简结构吗?
- 拿不准时,使用更少的嵌套层级
- 顶级路由更简洁直接
-
我使用的是Next.js 15+吗?
- 是:类型为
paramsPromise<{...}> - 否:类型为
params{...}
- 是:
Examples: Real-World Scenarios
示例:真实场景
E-commerce Product Page
电商产品页面
typescript
// 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,
};
}typescript
// 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 ?? '产品不存在',
description: product?.description,
};
}Documentation Site with Nested Paths
带嵌套路径的文档站点
typescript
// 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('/'),
}));
}typescript
// 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('/'),
}));
}Checklist for Dynamic Routes
动态路由检查清单
Before implementing a dynamic route, verify:
- Route structure is as simple as possible (default to top-level unless told otherwise)
- Not inferring route nesting from resource type names
- Route structure matches URL requirements (not over-nested)
- is typed as
paramsfor Next.js 15+Promise<{...}> - is awaited before accessing properties (Next.js 15+)
params - Error handling for invalid IDs/slugs (use )
notFound() - TypeScript types are properly defined
- Using Server Component unless client interactivity needed
- Consider for SSG if applicable
generateStaticParams() - Metadata generation if SEO matters
在实现动态路由前,验证以下内容:
- 路由结构尽可能简洁(默认使用顶级路由,除非有明确要求)
- 没有从资源类型名称推断路由嵌套
- 路由结构与URL需求匹配(没有过度嵌套)
- Next.js 15+中的类型为
paramsPromise<{...}> - Next.js 15+中访问属性前已await
params - 对无效ID/别名有错误处理(使用)
notFound() - 已正确定义TypeScript类型
- 除非需要客户端交互,否则使用Server组件
- 若适用,考虑使用实现SSG
generateStaticParams() - 若SEO重要,已实现元数据生成
Quick Reference
快速参考
| Scenario | Route Structure | Params Access |
|---|---|---|
| Single resource by ID | | |
| Category + resource | | |
| Blog with slugs | | |
| Nested resources | | |
| Flexible paths | | |
| Optional paths | | |
| Client Component | Use | |
Remember: Dynamic routes in Next.js are file-system based. The folder structure with creates the dynamic segments, and the prop (or hook) provides access to those values.
[brackets]paramsuseParams()| 场景 | 路由结构 | 参数访问方式 |
|---|---|---|
| 基于ID的单个资源 | | |
| 分类+资源 | | |
| 带别名的博客 | | |
| 嵌套资源 | | |
| 灵活路径 | | |
| 可选路径 | | |
| Client组件 | 使用 | |
记住: Next.js中的动态路由基于文件系统。带的文件夹结构创建动态片段,属性(或钩子)用于访问这些值。
[方括号]paramsuseParams()