Skip to content
This page has been auto-translated and may contain errors.View in English

搜索参数

路由参数告诉你的应用当前在哪个页面。搜索参数(也叫查询参数)描述这个页面应该是什么样子:哪个筛选条件活跃、列表怎样排序、正在显示第几页结果。它们出现在 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。

jsx
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 中获取。

jsx
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",但手工编辑的链接可能带的是 Sithsith,所以比较前两边都要转成小写。

把搜索参数放在 URL 里最直接的办法是用 Link,它的 to 以问号开头。React Router 看到开头的 ?,让你留在当前路由,换上新的查询字符串,组件重新渲染,你的筛选逻辑重新运行。

jsx
<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。

jsx
<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

jsx
// 在 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()}`
}
jsx
<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,用它的回调形式。回调接收前一个参数对象,你调整那一个键,返回它。

jsx
// 在 CharacterList 里,这样才能调用 setSearchParams
function handleFilterChange(key, value) {
  setSearchParams(prevParams => {
    if (value === null) {
      prevParams.delete(key)
    } else {
      prevParams.set(key, value)
    }
    return prevParams
  })
}
jsx
<button onClick={() => handleFilterChange('type', 'jedi')}>Jedi</button>
<button onClick={() => handleFilterChange('type', null)}>Clear</button>

这里有个惊喜,这门课会指出:与 useState 的 updater 不同(那里禁止修改前一个状态),这里直接在 prevParams 上调用 .delete.set 再返回它是没问题的。现在 links 和按钮都只改变它们自己拥有的参数。

这个修改是安全的,是因为 setter 实际做的事。当你发出一个 useState 更新时,React 用 Object.is 比较新值和当前值,然后才决定是否调度重新渲染,返回同一个被修改过的对象会被读作"没有变化",更新根本不会触发。setSearchParams 反而会触发导航:它序列化你返回的东西成一个新位置并推送它,没有身份比较来阻止。

回调接收的是什么

从 React Router 7.7.0 开始你得到一个当前参数的拷贝;之前从 6.4 版回调形式出现以来,你拿到的是组件正在渲染的那个活实例。修改你拿到的东西并返回它在两种情况下都能工作。

useState 的类比也在队列方面停止了:同一个处理器里的两个 setSearchParams 调用都从当前 URL 开始,所以第二个会覆盖第一个。一次调用里设置多个参数。每次设置,跟每次 Link 点击一样,默认会推送一个历史条目,对于每次按键都改变的参数,把 { replace: true } 作为 setter 的第二个参数。

实际中会出现两个更尖锐的陷阱。第一,查询字符串可以合法地重复某个键:?type=jedi&type=sith.get 只返回第一个值,.getAll 以数组形式返回全部,.delete(key) 删除这个键的每一个条目,所以上面的合并辅助函数会把重复的键折叠而不是单独管理它们。多选筛选需要 .getAll 加上 .append.delete(key, value) 只删除单个值,让那个键的其他条目保留。

第二,要抵抗把参数复制到 state 里的冲动。通过 effectsearchParams.get('type') 复制进 useState 会创建两个真实来源,在一次渲染中它们会不同步;在渲染时推导,就像这里的每个例子都做的,把 URL 作为唯一的权威,代价只是一次廉价的重新计算。

JunoURL 可以保存你的状态 有些状态理应在刷新后保留,也该在分享链接时被传递,比如激活的筛选。搜索参数把这些状态保存在 URL 中问号的后面,useSearchParams 让你读取它:searchParams.get('type') 返回值,或者当参数不在时返回 null

你可以用 Link 链接到一个查询字符串,或者用 setter 函数,然后根据 URL 说的内容筛选你的列表。

JunoURL 可以保存你的状态 用分享测试:如果重新访问这个链接应该重现这个视图,那这个状态属于搜索参数。

useSearchParams 读取,在组件顶部推导筛选列表,用 Link 设置参数用于固定的可见选项,用 setter 设置参数用于程序化的改变。

硬编码整个查询字符串会擦除不相关的参数,所以要合并:从当前参数的拷贝构建 Link 字符串,或者用 setter 的回调形式并只调整你拥有的那个键。

JunoURL 可以保存你的状态setSearchParams 是一次导航而不是状态更新,这就是为什么在回调里修改前一个 URLSearchParams 是安全的,以及为什么每次设置都会推送一个历史条目,除非你传递 replace: true

记住键可以重复,.get 只读第一个,.delete 删除全部,通过在渲染时推导而不是把参数复制到 state 来保持 URL 作为唯一的权威。

接下来:受保护的路由,应用的某个分支会先问你是谁。