用 TanStack Router 管理筛选和分页 URL
筛选条件全放在 useState 里时,刷新会丢,返回键也撤不回上一次筛选。把它们写进 URL 之后,链接可以复制,浏览器历史也能正常工作;接下来要处理的是参数类型、默认值和 loader 重载。
示例是一页产品列表,代码从 route 的 search schema 开始。
URL 里放什么
我通常看四件事:这个状态是否需要分享、刷新后是否要保留、返回键是否要撤销,以及服务端取数是否依赖它。只要其中一项成立,就值得考虑放进 URL。
q、page、sort、tags 应进入 URL;输入框是否聚焦、弹窗是否正在播放动画不应进入 URL。不要把整个组件状态树序列化进去。
最终地址类似:
search 校验
URL 是外部输入。用户可能手写 page=-4,旧书签里可能留着已经删除的 sort,第三方链接也可能传错类型。这些情况在路由入口统一处理,组件就不用各自修正一遍。
TanStack Router 会从 schema 推断 Route.useSearch() 的类型。加上 .catch() 后,错误参数会回落到预设值,别人粘贴一条坏 URL 也不会让整条路由进入错误页。
如果业务要求严格拒绝非法参数,可以去掉 catch 并提供 errorComponent。筛选页通常更适合容错,支付或权限回调则可能需要严格失败。
loaderDeps
loaderDeps 把 search 中与数据请求有关的字段显式交给 loader。依赖变化时路由会重载,过期 loader 的 abortController.signal 会被取消。
loaderDeps 不需要返回完整的 search 对象。比如面板展开状态不影响 API,却会让 loader 多跑一次。把依赖逐个写出来后,缓存键和请求次数也更容易查。
组件取值
组件直接读取路由状态和 loader 数据即可。再用 Effect 把 search.q 同步到全局 store,会多出一份需要手动保持一致的状态。
输入框可以暂时保留一份 draft,避免每敲一个字就导航和请求。等 250ms 后再把 draft 写回 URL;其他组件仍然读取 URL 里的查询值。
replace 与 push
实时输入时用 replace: true,避免历史记录出现 r、re、rea 等每一个中间值。切换分页不使用 replace,让返回键能回到上一页。
修改搜索词或标签时把 page 重置为 1。用户如果停在第 8 页再切筛选,很容易落到空页,看起来像是没有搜索结果。
函数式 search 更新里要保留 ...previous。直接返回 { page: 2 } 会替换其余 search 参数,查询词和标签也会一起消失。
默认参数
schema 的默认值让组件始终拿到完整类型,但不代表所有默认值都必须显示在地址栏。可以使用 TanStack Router 的 stripSearchParams 中间件去掉默认 page 和 sort,让分享链接更短。
我会等校验、更新和 loader 都跑通后再加这个中间件。调试阶段把默认参数留在地址栏里,反而更容易看出是哪一步把值改掉了。
测试
这几个路径比较容易漏:
- 打开
/products?page=abc,页面应回落到第 1 页。 - 连续输入搜索词,Network 中旧 loader 应被取消。
- 从第 3 页修改标签,页码应回到 1。
- 连点两次下一页,再按返回,应该逐页返回。
- 复制当前 URL 到无痕窗口,筛选结果应一致。
- 删除所有参数后刷新,默认状态应稳定且没有水合差异。
这些用例通过后,分享链接、刷新、SSR、loader 缓存和浏览器历史用的就是同一组参数。以后再加筛选项,也只需要沿着 schema、loaderDeps 和导航更新这条线改。
相关文档: