02. Router/Search:TanStack 真正锋利的刀

Next.js 的路由够用;TanStack Router 的路由是类型系统参与设计的状态机。差别不小,别装作只是文件名不同。

核心差异

问题Next.js 常见做法TanStack Router 做法
路径参数从 params/useParams 取字符串,自己转换路径、params、loader、Link 导航互相推导
Search paramsURLSearchParams/string 为主,类型靠约定JSON-first parse + validateSearch + 类型推导
数据加载server component/fetch/route handler/server action 多层模型route loader + beforeLoad + loaderDeps,生命周期清晰
链接安全一定程度靠插件/约定Link 目标和 search 结构可编译期检查

URL 是状态管理器,不是字符串垃圾桶

TanStack Router 把 search params 当成一等状态:可验证、可类型化、可继承、可参与 loader 缓存 key。

const productSearchSchema = z.object({
  page: z.number().catch(1),
  filter: z.string().catch(''),
  sort: z.enum(['newest', 'oldest', 'price']).catch('newest'),
})

export const Route = createFileRoute('/products')({
  validateSearch: productSearchSchema,
  loaderDeps: ({ search }) => ({
    page: search.page,
    filter: search.filter,
    sort: search.sort,
  }),
  loader: ({ deps }) => fetchProducts(deps),
})
练习:把你一个 Next.js 项目里的列表页 mentally 重构:哪些 UI state 应进入 URL?哪些只是本地 state?经验规则:刷新、分享、回退后还应保留的,优先考虑 URL。

loaderDeps 是缓存正确性的开关

一个常见错误:把整个 search 对象塞进 loaderDeps。这样任何无关参数变化都触发 reload。

// ❌ 过度依赖:任何 search 变化都 reload
loaderDeps: ({ search }) => search

// ✅ 只声明 loader 真正使用的依赖
loaderDeps: ({ search }) => ({ page: search.page, limit: search.limit })

Next.js 工程师迁移时的脑内替换

Next 习惯TanStack 替换
页面读取 searchParams 后手写 parseRoute.validateSearch
useRouter().push('/x?a=1')<Link to="/x" search={{ a: 1 }} />
页面级 fetchRoute.loader + loaderDeps
middleware/auth 分散处理beforeLoad / route context / middleware
快速判断

为什么不要把整个 search 对象直接作为 loaderDeps?

推荐原文

Search ParamsData Loading