Next.js 的路由够用;TanStack Router 的路由是类型系统参与设计的状态机。差别不小,别装作只是文件名不同。
| 问题 | Next.js 常见做法 | TanStack Router 做法 |
|---|---|---|
| 路径参数 | 从 params/useParams 取字符串,自己转换 | 路径、params、loader、Link 导航互相推导 |
| Search params | URLSearchParams/string 为主,类型靠约定 | JSON-first parse + validateSearch + 类型推导 |
| 数据加载 | server component/fetch/route handler/server action 多层模型 | route loader + beforeLoad + loaderDeps,生命周期清晰 |
| 链接安全 | 一定程度靠插件/约定 | Link 目标和 search 结构可编译期检查 |
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),
})一个常见错误:把整个 search 对象塞进 loaderDeps。这样任何无关参数变化都触发 reload。
// ❌ 过度依赖:任何 search 变化都 reload
loaderDeps: ({ search }) => search
// ✅ 只声明 loader 真正使用的依赖
loaderDeps: ({ search }) => ({ page: search.page, limit: search.limit })| Next 习惯 | TanStack 替换 |
|---|---|
| 页面读取 searchParams 后手写 parse | Route.validateSearch |
| useRouter().push('/x?a=1') | <Link to="/x" search={{ a: 1 }} /> |
| 页面级 fetch | Route.loader + loaderDeps |
| middleware/auth 分散处理 | beforeLoad / route context / middleware |
为什么不要把整个 search 对象直接作为 loaderDeps?