作者:Tanner Linsley 于 2025 年 6 月 3 日发布。
搜索参数在历史上一直被当作二等状态对待。它们是全局的、可序列化的,并且可共享 — 但在大多数应用中,它们仍然通过字符串解析、松散的约定和脆弱的工具来拼凑。
即使是像验证一个 sort 参数这样简单的操作,也会很快变得冗长
const schema = z.object({
sort: z.enum(['asc', 'desc']),
})
const raw = Object.fromEntries(new URLSearchParams(location.href))
const result = schema.safeParse(raw)
if (!result.success) {
// fallback, redirect, or show error
}
const schema = z.object({
sort: z.enum(['asc', 'desc']),
})
const raw = Object.fromEntries(new URLSearchParams(location.href))
const result = schema.safeParse(raw)
if (!result.success) {
// fallback, redirect, or show error
}
这可行,但它是手动且重复的。没有推断,与路由本身没有联系,并且一旦你想添加更多类型、默认值、转换或结构,它就会失效。
更糟糕的是,URLSearchParams 仅支持字符串。它不支持嵌套 JSON、数组(超出简单的逗号分隔)或类型转换。所以,除非你的状态是扁平且简单的,否则你会很快遇到瓶颈。
这就是为什么我们开始看到工具和提案的兴起 — 比如 Nuqs、Next.js RFCs 和社区模式 — 目的是让搜索参数更具类型安全性和人体工程学。其中大多数专注于改进从 URL 读取。
但是,几乎没有一个能解决更深层次、更难解决的问题:安全、原子化地写入搜索参数,并充分意识到路由上下文。
从 URL 读取是一回事。从代码中构建一个有效的、有目的的 URL 是另一回事。
当你尝试这样做时
<Link to="/dashboards/overview" search={{ sort: 'asc' }} />
<Link to="/dashboards/overview" search={{ sort: 'asc' }} />
你会意识到你不知道这个路由的搜索参数有哪些是有效的,或者你是否正确地格式化了它们。即使有辅助函数来字符串化它们,也没有什么能强制执行调用者和路由之间的契约。没有类型推断,没有验证,也没有防护措施。
这就是约束成为一项功能的地方。
除非在路由本身中显式声明搜索参数模式,否则你只能猜测。你可能在一个地方验证,但没有任何东西能阻止另一个组件使用无效、部分或冲突的状态进行导航。
约束是实现协调的关键。它使得非本地调用者能够安全地参与。
像 **Nuqs** 这样的工具就是一个很好的例子,说明了本地抽象如何能改善搜索参数处理的人体工程学。你获得了 Zod 支持的解析、类型推断,甚至可写 API — 所有这些都作用于特定的组件或钩子。
它们使得在隔离中读取和写入搜索参数变得更容易 — 这是有价值的。
但是,它们并没有解决更广泛的协调问题。你仍然会遇到重复的模式、不一致的期望,并且无法在路由或组件之间强制执行一致性。默认值可能会冲突。类型可能会漂移。当路由发生变化时,没有任何东西能保证所有调用者都会更新。
这就是真正的碎片化问题 — 解决它需要将搜索参数模式引入到路由层本身。
TanStack Router 整体上解决了这个问题。
与其在你的应用程序中分散模式逻辑,不如将其定义在路由本身内部
export const Route = createFileRoute('/dashboards/overview')({
validateSearch: z.object({
sort: z.enum(['asc', 'desc']),
filter: z.string().optional(),
}),
})
export const Route = createFileRoute('/dashboards/overview')({
validateSearch: z.object({
sort: z.enum(['asc', 'desc']),
filter: z.string().optional(),
}),
})
这个模式成为唯一的真实来源。你在任何地方都能获得完整的推断、验证和自动完成
<Link
to="/dashboards/overview"
search={{ sort: 'asc' }} // fully typed, fully validated
/>
<Link
to="/dashboards/overview"
search={{ sort: 'asc' }} // fully typed, fully validated
/>
想只更新搜索状态的一部分?没问题
navigate({
search: (prev) => ({ ...prev, page: prev.page + 1 }),
})
navigate({
search: (prev) => ({ ...prev, page: prev.page + 1 }),
})
它是 reducer 式的、事务性的,并且直接集成到路由的响应式模型中。组件仅在它们使用的特定搜索参数发生变化时重新渲染 — 而不是每次 URL 变动时。
当你的搜索参数逻辑存在于用户空间 — 分散在钩子、工具和助手之间 — 最终出现冲突的模式只是时间问题。
也许一个组件期望 `sort: 'asc' | 'desc'`。另一个添加了一个 `filter`。第三个默认假设 `sort: 'desc'`。它们都没有共享真实来源。
这会导致
TanStack Router 通过将模式直接绑定到你的路由定义 —分层地 — 来防止这种情况。
父路由可以定义共享的搜索参数验证。子路由可以继承该上下文,添加内容,或以类型安全的方式扩展它。这使得在应用程序的不同部分意外创建重叠、不兼容的模式成为不可能。
实际应用中它是这样工作的
// routes/dashboard.tsx
export const Route = createFileRoute('/dashboard')({
validateSearch: z.object({
sort: z.enum(['asc', 'desc']).default('asc'),
}),
})
// routes/dashboard.tsx
export const Route = createFileRoute('/dashboard')({
validateSearch: z.object({
sort: z.enum(['asc', 'desc']).default('asc'),
}),
})
然后子路由可以安全地扩展模式
// routes/dashboard/$dashboardId.tsx
export const Route = createFileRoute('/dashboard/$dashboardId')({
validateSearch: z.object({
filter: z.string().optional(),
// ✅ \`sort\` is inherited automatically from the parent
}),
})
// routes/dashboard/$dashboardId.tsx
export const Route = createFileRoute('/dashboard/$dashboardId')({
validateSearch: z.object({
filter: z.string().optional(),
// ✅ \`sort\` is inherited automatically from the parent
}),
})
当你匹配 `/dashboard/123?sort=desc&filter=active` 时,父路由验证 `sort`,子路由验证 `filter`,一切无缝协同工作。
尝试在子路由中将必需的父参数重新定义为完全不同的东西?类型错误。
validateSearch: z.object({
// ❌ Type error: boolean does not extend 'asc' | 'desc' from parent
sort: z.boolean(),
filter: z.string().optional(),
})
validateSearch: z.object({
// ❌ Type error: boolean does not extend 'asc' | 'desc' from parent
sort: z.boolean(),
filter: z.string().optional(),
})
这种强制执行使得嵌套路由既可以组合又安全 — 这是一种罕见的组合。
这里的魔力在于你不需要教你的团队遵循约定。路由拥有模式。每个人都在使用它。没有重复。没有漂移。没有隐性 bug。没有猜测。
当你将验证、类型和所有权引入路由本身时,你就停止把 URL 当作字符串,而是开始把它们当作真正的状态 — 因为它们就是。
大多数路由系统将搜索参数视为事后添加。你可以读取它,也许解析它,也许字符串化它,但很少有东西是你真正可以信任的。
TanStack Router 颠覆了这一点。它使搜索参数成为路由契约的核心部分 — 经过验证、可推断、可写入且具有响应性。
因为如果你不把搜索参数当作状态来对待,你就会不断地泄露它,破坏它,并绕过它。
最好从一开始就正确处理。
如果你对将搜索参数视为头等状态的可能性感到好奇,我们邀请你试用 TanStack Router。在你的路由逻辑中体验经过验证、可推断且具有响应性的搜索参数的强大功能。