TanStack Router:retainSearchParams 搜索中间件——让 Search Params 在导航间自动保留
【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router
在 TanStack Router 中,每次客户端导航(link/navigate)都会按目标路由重新解析 search params,当前 URL 中的查询参数(如分页页码、筛选条件、会话标识)很容易被"丢掉"。retainSearchParams是一个开箱即用的search middleware:把它挂到路由的search.middlewares上后,指定的搜索参数(或全部参数)会在后续导航中自动保留,无需在每处to/search里手动透传。本文基于仓库中 retainSearchParamsFunction.md 的官方文档,结合 router-core 源码 与测试用例,完整讲解它的用法、参数语义和底层工作原理。
读完本文,你将能够:
- 在 code-based(
createRootRoute/createRoute)与 file-based(createFileRoute)路由上正确配置retainSearchParams; - 理解它
true与 key 数组两种入参的行为差异,以及"显式导航值优先"的保留规则; - 看懂
SearchMiddleware类型、中间件执行管线与meta元数据在参数保留/剔除中的作用。
retainSearchParams 的入参:true 或 key 列表
retainSearchParams接受两种形式的keys:
true:保留当前 search 中的所有参数;Array<keyof TSearchSchema>:只保留列表里列出的 key(类型上被约束为当前路由 search schema 的 key)。
示例一:保留指定 key(根路由,code-based)
官方文档给出的第一个示例是在根路由上保留rootValue:
import { z } from 'zod' import { createRootRoute, retainSearchParams } from '@tanstack/react-router' const searchSchema = z.object({ rootValue: z.string().optional(), }) export const Route = createRootRoute({ // Zod v4 可以直接传 schema validateSearch: searchSchema, search: { middlewares: [retainSearchParams(['rootValue'])], }, })这里middlewares是search配置项下的数组,retainSearchParams(['rootValue'])表示:后续任何导航中,只要目标 search 没有显式指定rootValue,就沿用当前 URL 里的rootValue。
示例二:保留全部参数(file-based 路由)
第二个示例演示了true入参以及 file-based 路由写法:
import { z } from 'zod' import { createFileRoute, retainSearchParams } from '@tanstack/react-router' const searchSchema = z.object({ one: z.string().optional(), two: z.string().optional(), }) export const Route = createFileRoute('/')({ // Zod v4 可以直接传 schema validateSearch: searchSchema, search: { middlewares: [retainSearchParams(true)], }, })传true时,one、two等全部参数在导航后都会保留(显式覆盖的除外)。
注意两点适用前提:
validateSearch使用 Zod 时,文档注释明确提示"Zod v4 可以直接使用 schema",旧版 Zod 通常需要包一层zodValidator(searchSchema)(仓库文档中以 Zod v4 为准);retainSearchParams挂在哪个路由上,就在该路由参与的目标路由链路中生效。中间件会按目标路由链从外到内收集(见下文applySearchMiddleware源码分析),因此把"全局性"的保留逻辑放在根路由上是最常见的做法。
源码解析:retainSearchParams 的实现
retainSearchParams的核心实现在 searchMiddleware.ts(router-core包,React / Solid / Vue 三端均从router-core再导出,例如 react-router/src/index.tsx):
export function retainSearchParams<TSearchSchema extends object>( keys: Array<keyof TSearchSchema> | true, ): SearchMiddleware<TSearchSchema> { return ({ search, next }) => { const { search: resultSearch, meta } = ( next as unknown as SearchMiddlewareNextWithMeta<TSearchSchema> )(search, true) if (keys === true) { const copy = { ...search, ...resultSearch } // 1) 处理"被默认值填充/被移除"的 key:显式导航值或值未变化时删除, // 让保留逻辑尊重导航方显式给出的参数 // 2) meta.removedAny 中的 key 无条件删除 // 3) 被 validateSearch 用默认值填充的 key,如果当前 search 有真实值, // 则用当前值覆盖默认值 return copy } // keys 数组分支:以导航结果 resultSearch 为底, // 把列表中的 key 从当前 search 补回去(同样尊重显式值与默认值) const copy = { ...resultSearch } // ... 逐 key 补回逻辑 return copy } }关键设计点:
- 不可变(不 mutate):两个分支都先展开成新对象(
{ ...search, ...resultSearch }或{ ...resultSearch }),从不修改传入的search。router-core 单元测试专门验证了这一点:retainSearchParams(['id', 'filter'])执行后原 search 对象保持不变,返回{ id: '1', filter: 'active', page: 'new' }。 - 显式导航值优先:源码中通过
meta.explicit(本次导航显式声明的 search)与deepEqual比对判断——如果导航显式设置了某个参数(哪怕与当前值相同),就不会被"保留"覆盖;这保证了业务代码永远可以用显式传参打断保留行为。 - 与 schema 默认值协同:
meta.defaulted记录哪些 key 是被validateSearch用默认值(如 Zod 的.default())填充的,retain 会用当前 URL 中的真实值替换这些默认值,避免"默认值把保留的参数顶掉"。
中间件管线:SearchMiddleware 类型与执行顺序
类型定义
中间件相关类型定义在 route.ts:
export type SearchMiddlewareMeta = { removed?: Map<string, unknown> // 被"按值"移除的 key 及其默认值 removedAny?: Set<string> // 被"无条件"移除的 key defaulted?: Map<string, unknown> // 被 schema 默认值填充的 key explicit?: unknown // 本次导航显式声明的 search 结果 } export type SearchMiddlewareContext<TSearchSchema> = { search: TSearchSchema // 当前 URL 的 search next: (newSearch: TSearchSchema) => TSearchSchema // 调用后续中间件 meta?: SearchMiddlewareMeta } export type SearchMiddleware<TSearchSchema> = ( ctx: SearchMiddlewareContext<TSearchSchema>, ) => TSearchSchema这是一个经典的洋葱模型:search是当前 URL 的参数,next把(可能被修改过的)search 传给后续中间件,最终由路由配置的search函数式更新(dest.search)产生导航声明的目标参数。retainSearchParams内部调用next(search, true)(第二个参数collectMeta: true)拿到"后续管线算出的目标 search + 元数据",再在此基础上做保留——即它站在导航结果之前做后处理,只补回它负责的 key。
执行管线 applySearchMiddleware
管线装配与执行逻辑在 router.ts 的applySearchMiddleware中,导航构建目标 location 时调用(调用点见 router.ts#L2063):
- 收集中间件:遍历目标路由链
destRoutes,依次push每个路由routeOptions.search.middlewares;旧的preSearchFilters/postSearchFilters会被包装成等价中间件兼容(源码注释标明将在 v2 移除); - 追加 validate 中间件:当路由配置了
validateSearch时,动态追加一个 validate 中间件——它先next(search)拿到导航结果,再执行 schema 校验/解析,并把"结果中出现但输入中没有的 key"记录到meta.defaulted(即被默认值填充的参数),最后返回{ ...result, ...validated }。这解释了上文"与 schema 默认值协同"的行为来源; - 末端落地:中间件链耗尽后(
index >= middlewares.length),若dest.search为函数/对象,则执行functionalUpdate(dest.search, currentSearch)得到最终声明值,并写入meta.explicit。
从源码结构看,这意味着retainSearchParams拿到的resultSearch是"本次导航显式声明 + schema 校验后"的结果,而meta.explicit/meta.defaulted让它能区分"导航显式给的值"和"schema 默认值",从而实现精确的保留语义。
行为验证:官方测试用例
router-core 单元测试
searchMiddleware.test.ts 覆盖了三种关键行为:
- 数组 key + next 不返回这些 key:
retainSearchParams(['id', 'filter'])下,当前 search 为{ id: '1', filter: 'active', page: '2' }、导航结果为{ page: 'new' }时,最终得到{ id: '1', filter: 'active', page: 'new' }——保留项被补回,导航项正常更新; true全量保留:当前 search 为{ id: '1', filter: 'active' }、导航结果为{ id: '2' }时,retainSearchParams(true)输出{ id: '2', filter: 'active' }——显式更新的id生效,未被提及的filter被保留;- 同引用复用安全:同一 search 对象多次调用中间件,结果一致且不产生变异。
三端集成测试(真实导航场景)
react-router 的集成测试 用真实路由(根路由/+/posts)验证:
- 初始 search 为
{ value: 'abc' },点击 link 导航到/posts后,router.state.location.search与 URL 中value=abc仍然存在(should retain value search param); - 初始 search 为空时,
retainSearchParams(['value'])"什么都不做",导航后 search 为空(should do nothing if value search param is not set)。
Solid 与 Vue 端拥有同构的集成测试:solid-router/tests/searchMiddleware.test.tsx 与 vue-router/tests/searchMiddleware.test.tsx,说明该中间件行为在三个框架绑定层保持一致。
与 stripSearchParams 的对照
与retainSearchParams同文件实现的 stripSearchParams 方向相反:它在导航时剔除参数(支持true全量剔除、key 数组、以及"值等于默认值才剔除"的对象形式),并通过meta.removed/meta.removedAny把剔除信息暴露给后续中间件——retainSearchParams正是读取这两个字段来决定"保留的 key 是否已被上游明确移除"。两者组合使用可以精细控制 URL 的"带什么、丢什么",其用法详见 stripSearchParamsFunction.md。
小结与适用要点
| 要点 | 说明 |
|---|---|
| 挂载位置 | 路由选项search.middlewares数组,根路由挂"全局保留",子路由挂"局部保留" |
| 入参 | true(保留全部)或keyof TSearchSchema数组(保留指定 key),key 受 search schema 类型约束 |
| 显式优先 | 导航中显式声明的参数会覆盖保留逻辑(由meta.explicit判定) |
| 默认值协同 | 被 schema 默认值填充的 key 会用当前 URL 真实值还原(由meta.defaulted判定) |
| 不可变 | 中间件不修改原 search 对象,测试中有专门断言 |
| 框架一致性 | 实现在router-core,React / Solid / Vue 三端同构导出、各有集成测试 |
如果你希望在跨页导航中保持分页、筛选、来源追踪等 query 参数不丢失,retainSearchParams就是 TanStack Router 提供的官方手段:一行middlewares: [retainSearchParams([...])]即可声明,底层由 searchMiddleware.ts 与 router.ts 中的中间件管线 共同保证正确性。
【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考