搜索参数
路由参数告诉你的应用当前在哪个页面。搜索参数(也叫查询参数)描述这个页面应该是什么样子:哪个筛选条件活跃、列表怎样排序、正在显示第几页结果。它们出现在 URL 中问号的后面,以键值对的形式存在,比如 /vans?type=rugged,多个参数之间用 & 连接:/vans?type=rugged&sort=price。因为它们是 URL 的一部分,刷新页面时会保留下来,分享链接时也会被传递,这使它们与 useState 能管理的任何状态都不同。
应该放在 URL 里的状态
用 useState 保存的状态存在内存中。刷新页面时它会重置为初始值;把 URL 复制给朋友时,他们会从头开始,看不到你的任何选择。对很多状态来说这样很好。但对某些状态,这是个真正的遗憾:如果你把厢式车列表筛选到只显示坚固型且价格在一定范围内的,你可能想刷新后保留这个视图,也希望别人打开粘贴的链接时看到同样精选过的列表。
这门课提供了一个有用的测试方法:用户能否重新访问或分享这个页面,让它看起来完全一样并得到相同的结果?如果答案是"是",就考虑把这块状态从 React 移到 URL 里作为搜索参数。筛选、排序和分页是经典的候选项。这样 URL 就成了这块状态的唯一真实来源,你的组件根据它来决定渲染什么,就像从普通的 state 或 props 推导一样。
用 useSearchParams 读取参数
React Router 通过 useSearchParams hook 暴露查询字符串,它的形式刻意设计得接近 useState:一个数组,包含当前值和一个 setter。
import { useSearchParams } from 'react-router-dom'
export default function CharacterList() {
const [searchParams, setSearchParams] = useSearchParams()
const typeFilter = searchParams.get('type')
// ...
}searchParams 是浏览器原生 URLSearchParams 对象的实例而不是普通对象,所以你通过它的方法来交互。.get('type') 以字符串形式返回 type 参数的值,当这个参数根本不在 URL 里时返回 null。那个 null 就是你的代码用来判断没有激活任何筛选的方式。.toString() 把整套参数序列化回查询字符串,比如 type=sith&sort=price,不包括开头的问号。
版本说明
这里的例子从 react-router-dom 导入,同时支持 v6 和 v7;Routing 介绍了 v7 如何改变了包。
从参数筛选列表
有了参数以后,筛选就是组件顶部的纯 JavaScript。没有 state,没有 effect:读取参数和筛选数组都很快,所以在每次渲染时重复做这个工作没问题,让结果从 URL 中获取。
const typeFilter = searchParams.get('type')
const displayedCharacters = typeFilter
? characters.filter(char => char.type.toLowerCase() === typeFilter.toLowerCase())
: characters
const charEls = displayedCharacters.map(char => (
<li key={char.name}>{char.name}</li>
))三元表达式处理没有筛选的情况:当 .get 返回 null 时,显示完整列表。选择用这种方式渲染哪个数组,跟你在条件渲染中用过的"边推导边渲染"的思路一样,只不过驱动数据来自 URL 而不是 state。注意大小写问题;数据里存的是 "Sith",但手工编辑的链接可能带的是 Sith 或 sith,所以比较前两边都要转成小写。
用 Links 设置参数
把搜索参数放在 URL 里最直接的办法是用 Link,它的 to 以问号开头。React Router 看到开头的 ?,让你留在当前路由,换上新的查询字符串,组件重新渲染,你的筛选逻辑重新运行。
<Link to="?type=jedi">Jedi</Link>
<Link to="?type=sith">Sith</Link>
<Link to=".">Clear</Link>清空筛选用 to=".",它导航到当前路径且不带任何查询;to="" 也能工作,这门课选择用点号因为它更明确。当筛选是一个固定的可见选项集合时,Links 最合适:它们渲染成真实的 anchor 标签,用户可以在新标签页打开筛选后的视图或在点击前复制地址。
用 setter 函数设置参数
useSearchParams 返回的第二个元素是一个 setter,跟 useState 里的 setter 一样,它接受替换值或回调。因为按钮不像 Link 那样是 React Router 生态的一部分,你需要从普通事件处理器里调用这个 setter。
<button onClick={() => setSearchParams({ type: 'jedi' })}>Jedi</button>
<button onClick={() => setSearchParams({ type: 'sith' })}>Sith</button>
<button onClick={() => setSearchParams({})}>Clear</button>这个 setter 对输入很灵活:像 '?type=jedi' 这样的字符串(有没有问号都行)能工作,但这里展示的对象形式是你最常看到的,空对象可以清空所有参数。当新参数来自逻辑而不是点击固定选项时,可以用 setter:从表单读取值、响应用户输入、一次性设置多个参数。
替换会清除其他参数
到目前为止两种方法都是硬编码完整的查询字符串。在你的应用只有 type 这一个参数时没问题,但一旦 URL 还带了其他不相关的东西,比如 ?name=jill&type=jedi,点击这些 links 或按钮就会替换整个查询字符串,name=jill 消失了。那些清空按钮更粗暴:它们擦除 URL 里的每一个参数,包括这个组件从来没碰过的。如果你确定项目永远只会有那一个参数,硬编码没问题。否则你需要合并。
与现有参数合并
对 Links 来说,to 是个字符串,所以解决办法是一个小辅助函数,在渲染时运行:把当前参数复制进一个新的 URLSearchParams,改变那个正在移动的键,序列化结果。它放在组件内部,因为它需要读取 hook 中的 searchParams。
// 在 CharacterList 里,这样才能读取 searchParams
function genNewSearchParamString(key, value) {
const sp = new URLSearchParams(searchParams)
if (value === null) {
sp.delete(key)
} else {
sp.set(key, value)
}
return `?${sp.toString()}`
}<Link to={genNewSearchParamString('type', 'jedi')}>Jedi</Link>
<Link to={genNewSearchParamString('type', 'sith')}>Sith</Link>
<Link to={genNewSearchParamString('type', null)}>Clear</Link>这是纯 JavaScript,React Router 没有提供专门的东西。URLSearchParams 构造函数很乐意以现有的参数对象作为起点,.set 更新或添加一个键,传递 null 告诉辅助函数要 .delete 这个键,所以现在"Clear"只删除 type,URL 里其他东西都保留。
对 setter,用它的回调形式。回调接收前一个参数对象,你调整那一个键,返回它。
// 在 CharacterList 里,这样才能调用 setSearchParams
function handleFilterChange(key, value) {
setSearchParams(prevParams => {
if (value === null) {
prevParams.delete(key)
} else {
prevParams.set(key, value)
}
return prevParams
})
}<button onClick={() => handleFilterChange('type', 'jedi')}>Jedi</button>
<button onClick={() => handleFilterChange('type', null)}>Clear</button>这里有个惊喜,这门课会指出:与 useState 的 updater 不同(那里禁止修改前一个状态),这里直接在 prevParams 上调用 .delete 和 .set 再返回它是没问题的。现在 links 和按钮都只改变它们自己拥有的参数。
useSearchParams 让你读取它:searchParams.get('type') 返回值,或者当参数不在时返回 null。 你可以用 Link 链接到一个查询字符串,或者用 setter 函数,然后根据 URL 说的内容筛选你的列表。
接下来:受保护的路由,应用的某个分支会先问你是谁。

