nextjs-dynamic-routes-params

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Next.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
    params
    prop in page.tsx, layout.tsx, or route.ts
在以下场景中使用本技能:
  • 创建动态路由片段(例如: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 (
[param]
) whenever the UI depends on part of the pathname. Typical signals include:
  • 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 identifiers
Example requirements that lead to dynamic routes
  1. “Show a product page that loads whichever product ID appears in the URL” →
    app/[id]/page.tsx
    or
    app/products/[id]/page.tsx
  2. “Render a blog article based on its slug” →
    app/blog/[slug]/page.tsx
    or
    app/[slug]/page.tsx
  3. “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  ← 无法获取基于路径的标识符
会导向动态路由的需求示例
  1. “展示产品页面,加载URL中对应ID的产品” →
    app/[id]/page.tsx
    app/products/[id]/page.tsx
  2. “根据别名渲染博客文章” →
    app/blog/[slug]/page.tsx
    app/[slug]/page.tsx
  3. “支持嵌套文档,例如/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
app/[id]/page.tsx
or
app/[slug]/page.tsx
unless:
  • 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" →
    app/[id]/page.tsx
    ✅ (not
    app/products/[id]
    )
  • "Show user profile" →
    app/[userId]/page.tsx
    ✅ (not
    app/users/[userId]
    )
  • "Display blog post" →
    app/[slug]/page.tsx
    ✅ (not
    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.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

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/c
Key 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
/products/[id]
or
/users/[id]
. Unless explicitly told otherwise, prefer the simplest structure.
When deciding on route structure:
  1. 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:
      /123
      for any resource,
      /abc-def
      for slugs
    • Pattern: The ID/slug is the only identifier needed
    • When in doubt, choose this!
  2. 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
      ,
      /blog/my-post
      (when specified)
    • Pattern: Category + identifier (when both are required)
  3. Multi-segment dynamic (
    app/[cat]/[id]/page.tsx
    )
    • Use when hierarchy matters
    • Examples:
      /shop/electronics/123
    • Pattern: Multiple levels of categorization
⚠️ 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:
  • 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]
除非有明确要求,否则优先选择最简结构。
选择路由结构时:
  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获取产品” →
app/products/[id]/page.tsx
正确: “根据ID获取产品” →
app/[id]/page.tsx
错误: “为用户创建动态路由” →
app/users/[userId]/page.tsx
正确: “为用户创建动态路由” →
app/[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+,
params
is a Promise and must be awaited!
typescript
// ✅ 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+中,
params
是一个Promise,必须使用await!
typescript
// ✅ 正确写法 - 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
params
directly in Client Components.
Instead:
  1. Use
    useParams()
    hook:
typescript
'use client';

import { useParams } from 'next/navigation';

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

  // Use the id...
}
  1. 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
。替代方案:
  1. 使用
    useParams()
    钩子:
typescript
'use client';

import { useParams } from 'next/navigation';

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

  // 使用id...
}
  1. 从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: /123
typescript
// ❌ 不必要的嵌套
// app/products/product/[id]/page.tsx
// URL: /products/product/123

// ✅ 更简洁的写法
// app/products/[id]/page.tsx
// URL: /products/123

// 如果产品是主要资源,甚至可以更简洁:
// app/[id]/page.tsx
// URL: /123

Pitfall 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:
  1. What's the URL structure?
    • Single ID:
      [id]
    • Category + ID:
      category/[id]
    • Hierarchical:
      [category]/[id]
    • Flexible paths:
      [...slug]
  2. Is this a Server or Client Component?
    • Server: Use
      params
      prop (await it in Next.js 15+)
    • Client: Use
      useParams()
      hook
  3. Do I need the simplest structure?
    • When in doubt, use fewer nesting levels
    • Top-level routes are simpler and more direct
  4. Am I on Next.js 15+?
    • Yes:
      params
      is
      Promise<{...}>
    • No:
      params
      is
      {...}
当你需要创建动态路由时,问自己以下问题:
  1. URL结构是什么?
    • 单个ID:
      [id]
    • 分类+ID:
      category/[id]
    • 层级结构:
      [category]/[id]
    • 灵活路径:
      [...slug]
  2. 这是Server组件还是Client组件?
    • Server组件:使用
      params
      属性(Next.js 15+中需await)
    • Client组件:使用
      useParams()
      钩子
  3. 我需要最简结构吗?
    • 拿不准时,使用更少的嵌套层级
    • 顶级路由更简洁直接
  4. 我使用的是Next.js 15+吗?
    • 是:
      params
      类型为
      Promise<{...}>
    • 否:
      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)
  • params
    is typed as
    Promise<{...}>
    for Next.js 15+
  • params
    is awaited before accessing properties (Next.js 15+)
  • Error handling for invalid IDs/slugs (use
    notFound()
    )
  • TypeScript types are properly defined
  • Using Server Component unless client interactivity needed
  • Consider
    generateStaticParams()
    for SSG if applicable
  • Metadata generation if SEO matters
在实现动态路由前,验证以下内容:
  • 路由结构尽可能简洁(默认使用顶级路由,除非有明确要求)
  • 没有从资源类型名称推断路由嵌套
  • 路由结构与URL需求匹配(没有过度嵌套)
  • Next.js 15+中
    params
    的类型为
    Promise<{...}>
  • Next.js 15+中访问属性前已await
    params
  • 对无效ID/别名有错误处理(使用
    notFound()
  • 已正确定义TypeScript类型
  • 除非需要客户端交互,否则使用Server组件
  • 若适用,考虑使用
    generateStaticParams()
    实现SSG
  • 若SEO重要,已实现元数据生成

Quick Reference

快速参考

ScenarioRoute StructureParams 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
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 ComponentUse
useParams()
hook
const params = useParams<{ id: string }>()

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.
场景路由结构参数访问方式
基于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
Client组件使用
useParams()
钩子
const params = useParams<{ id: string }>()

记住: Next.js中的动态路由基于文件系统。带
[方括号]
的文件夹结构创建动态片段,
params
属性(或
useParams()
钩子)用于访问这些值。