N04第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

动态参数与搜索参数

动态参数与搜索参数 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

路由携带的两类参数要分清:路径里的动态段(params)标识「哪门课」,问号后面的搜索参数(searchParams)表达「怎么筛、翻到哪」。读完你应该能 await 两者、用链接承载筛选分页,并验证刷新和分享后状态完整恢复。

问题场景

课程列表页要支持「按方向筛选 + 分页」,比如 /courses?track=react&page=2。第一反应是用 useState 存筛选状态——能用,但刷新归零、分享出去就是裸链接,对方看到的是默认列表。筛选和分页本质是「页面从哪读」的输入,写进 URL 才对。

核心解释

服务端页面组件同时拿到两类参数,两者都是 Promise,都要 await:

// app/courses/page.tsx
type Props = {
  params: Promise<{}>;                                          // 路径参数:/courses/react 的 react
  searchParams: Promise<Record<string, string | string[] | undefined>>;
};

export default async function CoursesPage({ params, searchParams }: Props) {
  const { track = "all" } = await searchParams;   // ?track=react&page=2
  const page = Number((await searchParams).page ?? "1");

  const filtered = track === "all" ? COURSES : COURSES.filter(c => c.track === track);
  // 按 page 切片后渲染……
}

关键认知:searchParams 变化 = 导航。点一个带新查询参数的 Link,Next.js 向服务端重新请求这个页面的渲染结果,组件带着新参数重渲染。状态在 URL 里,所以:

  • 刷新页面,服务端从同一个 URL 读到同样的参数——状态恢复;
  • 复制链接发给别人,对方打开看到的就是你筛选后的列表;
  • 浏览器前进后退,参数随之变化,列表跟着变。

生成筛选链接的写法(服务端组件里直接算):

<div>
  {["all", "react", "nextjs"].map(t => (
    <Link
      key={t}
      href={t === "all" ? "/courses" : `/courses?track=${t}&page=1`}
      aria-current={track === t ? "true" : undefined}
    >
      {t}
    </Link>
  ))}
</div>

客户端组件里则用 useSearchParams() 读当前查询参数,配合 router.push 或 Link 拼新地址——两条路按组件位置选,别在服务端组件里用 Hook。

示例拆解

落地清单与验证:

  • 列表页同时消费 track(筛选)和 page(分页)两个查询参数,越界 page 回落到 1;
  • 每个筛选项是一个 Link,当前项有高亮标记;
  • 分页条也是 Link:?track=react&page=2,保留当前筛选只翻页;
  • 验证一:点筛选和翻页后按 F5 刷新,列表还在同一筛选、同一页;
  • 验证二:把当前地址复制到新标签页打开,状态一致;
  • 验证三:详情页 /courses/[slug] 用 await params 读课程标识,确认两类参数分工明确。

动手任务

在项目里完成三个小改动:

  1. 列表页实现 track 筛选 + page 分页,全部由查询参数驱动。
  2. 给「下一页 / 上一页」做成保留筛选的 Link,第一页隐藏「上一页」。
  3. 在浏览器直接改 URL 里的 page=99,确认页面回落到合法值而不是报错。

常见错误

  • searchParams 不 await 直接点属性:拿到的是 Promise,读出来 undefined;Next.js 15+ 必须 await。
  • 用 useState 存筛选分页:刷新即丢、链接不可分享;能进 URL 的 UI 状态尽量进 URL。
  • 在服务端组件里用 useSearchParams:它是客户端 Hook;服务端读参数用 await searchParams。
  • 参数类型想当然:查询参数永远是字符串(?page=2 来的是 "2"),且同名多值时是数组;用之前先转数字、先判断类型。

检查与复盘

完成下方检查题确认理解。自查清单:

  • params 与 searchParams 的分工说得清:身份标识 vs 视图状态
  • 两个参数都 await 后使用,类型处理过字符串默认值
  • 刷新、新标签页打开、前进后退三条路径都验证过状态恢复

参考资料

检查题共 3 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1读代码简单

阅读代码(Next.js 16,动态路由 app/courses/[slug]/page.tsx)。访问 /courses/r08-state-snapshot 时页面显示什么?

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  return <h1>课程:{slug}</h1>;
}
Q2单选中等

课程站的列表页支持按关键词筛选。为什么把筛选词放进 URL 的 searchParams,而不是组件的 useState?

Q3读代码较难

阅读代码(Next.js 16,客户端组件)。页面能正常构建和渲染。访问 /notes?q=sqlite 时页面上显示什么?

'use client';

export default function NotesPage({
  searchParams,
}: {
  searchParams: Promise<{ q?: string }>;
}) {
  const { q } = searchParams; // 没有 use() 也没有 await
  return <p>筛选词:{q ?? '(无)'}</p>;
}

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...