前言
之前介绍过 TanStack
,最近发现 TanStack Router
更新了一篇博客,介绍了搜索参数解析的新理念!今天分享给大家~,下面是翻译之后的原文!
往期精彩推荐
- 🚀🚀🚀这些高效实用的 vite 插件你一定要知道!更新弹窗、mock数据、开发快捷键、自动导入...
- 🚀🚀🚀 尤雨溪宣布 Vite 发布 Rolldown-Vite 预览版,性能超级快!⚡️⚡️⚡️
- ⚡️Vitest 3.2 发布,测试更高效;🚀Nuxt v4 测试版本发布,焕然一新;🚗Vite7 beta 版发布了
- 更多精彩文章欢迎关注我的公众号:萌萌哒草头将军
正文
搜索参数即状态
搜索参数历来被视为二等状态。它们是全局的、可序列化的、可共享的------但在大多数应用程序中,它们仍然通过字符串解析、松散的约定和脆弱的实用工具拼凑而成。
即使是像验证 sort
参数这样简单的事情,也很快变得冗长:
ts
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) {
// 回退、重定向或显示错误
}
这种方法有效,但它是手动的且重复的。没有类型推断、与路由本身没有关联,而且一旦你想添加更多类型、默认值、转换或结构,它就会崩溃。
更糟糕的是,URLSearchParams
只支持字符串。它不支持嵌套 JSON、数组(除了简单的逗号分割外)或类型强制转换。因此,除非你的状态是扁平且简单的,否则你很快就会遇到瓶颈。
这就是为什么我们开始看到工具和提案的兴起------比如 Nuqs、Next.js RFCs 和用户自定义模式------旨在使搜索参数更具类型安全性和人体工程学。这些大多专注于改进从 URL 的 读取。
但几乎没有哪一个解决了更深层次、更困难的问题:写入 搜索参数,以安全和原子化的方式,充分了解路由上下文。
写入搜索参数是问题所在
从 URL 读取是一回事。从代码中构建一个有效且有意的 URL 是另一回事。
当你尝试这样做时:
tsx
<Link to="/dashboards/overview" search={{ sort: 'asc' }} />
你会意识到你根本不知道这个路由支持哪些搜索参数,或者你是否正确地格式化了它们。即使有助手来将它们字符串化,也没有任何机制来强制调用者和路由之间的契约。没有类型推断、没有验证、没有护栏。
这就是 约束成为特性 的地方。
如果不在路由本身中明确声明搜索参数模式,你就只能猜测。你可能在一个地方进行了验证,但没有什么能阻止另一个组件使用无效、部分或冲突的状态进行导航。
约束是协调成为可能的关键。它使 非本地调用者 能够安全参与。
本地抽象有所帮助 ------ 但它们无法协调
像 Nuqs 这样的工具是本地抽象如何改善搜索参数处理 人体工程学 的绝佳例子。你可以获得基于 Zod 的解析、类型推断,甚至是可写的 API------所有这些都限定在特定组件或钩子中。
它们使在 隔离 中读写搜索参数变得更容易------这很有价值。
但它们无法解决更广泛的 协调 问题。你仍然会遇到重复的模式、分散的期望,以及无法在路由或组件之间强制一致性的问题。默认值可能冲突。类型可能漂移。当路由演变时,没有什么能保证所有调用者都会随之更新。
这才是真正的碎片化问题------解决它需要将搜索参数模式引入路由层本身。
TanStack Router 如何解决这个问题
TanStack Router 提供了整体解决方案。
你无需在应用程序中分散模式逻辑,而是 在路由本身中定义它:
ts
export const Route = createFileRoute('/dashboards/overview')({
validateSearch: z.object({
sort: z.enum(['asc', 'desc']),
filter: z.string().optional(),
}),
})
这个模式成为唯一的真相来源。你在任何地方都能获得完整的推断、验证和自动补全:
tsx
<Link
to="/dashboards/overview"
search={{ sort: 'asc' }} // 完全类型化,完全验证
/>
想只更新部分搜索状态?没问题:
ts
navigate({
search: (prev) => ({ ...prev, page: prev.page + 1 }),
})
它是 reducer 风格的、事务性的,并直接与路由器的响应性模型集成。组件仅在它们使用的特定搜索参数发生变化时才会重新渲染------而不是每次 URL 发生变化时。
TanStack Router 如何防止模式碎片化
当你的搜索参数逻辑存在于用户空间------分散在钩子、实用工具和助手函数中------不可避免地会出现 冲突的模式。
也许一个组件期望 sort: 'asc' | 'desc'
。另一个添加了 filter
。第三个假定 sort: 'desc'
为默认值。它们之间没有共享的真相来源。
这会导致:
- 不一致的默认值
- 冲突的格式
- 设置了其他组件无法解析的值的导航
- 损坏的深度链接和无法追踪的错误
TanStack Router 通过将模式直接绑定到路由定义------以 层级方式 防止这种情况。
父路由可以定义共享的搜索参数验证。子路由继承该上下文,以类型安全的方式添加或扩展它。这使得在应用程序的不同部分意外创建重叠、不兼容的模式变得 不可能。
示例:安全的分层搜索参数验证
以下是实际操作方式:
ts
// routes/dashboard.tsx
export const Route = createFileRoute('/dashboard')({
validateSearch: z.object({
sort: z.enum(['asc', 'desc']).default('asc'),
}),
})
然后,子路由可以安全地扩展该模式:
ts
// routes/dashboard/$dashboardId.tsx
export const Route = createFileRoute('/dashboard/$dashboardId')({
validateSearch: z.object({
filter: z.string().optional(),
// ✅ `sort` 从父路由自动继承
}),
})
当你匹配 /dashboard/123?sort=desc&filter=active
时,父路由验证 sort
,子路由验证 filter
,一切无缝协作。
尝试在子路由中将所需的父参数重新定义为完全不同的内容?会触发类型错误。
ts
validateSearch: z.object({
// ❌ 类型错误:布尔值无法扩展父路由的 'asc' | 'desc'
sort: z.boolean(),
filter: z.string().optional(),
})
这种强制执行使嵌套路由既可组合又安全------这是一种罕见的组合。
内置纪律
这里的魔法在于,你无需教导团队遵循约定。路由 拥有 模式。每个人只需使用它。没有重复。没有漂移。没有无声的错误。没有猜测。
当你将验证、类型化和所有权引入路由器本身时,你就不再将 URL 当作字符串,而是开始将其视为真正的状态------因为它们就是状态。
搜索参数即状态
大多数路由系统将搜索参数视为事后补充。你 可以 读取它们,也许可以解析,也许可以字符串化,但很少有你真正可以 信任 的东西。
TanStack Router
颠倒了这一观念。它使搜索参数成为路由契约的核心部分------经过验证、可推断、可写且具有响应性。
因为如果你不将搜索参数视为状态,你就会不断泄露它、破坏它并绕过它。
最好从一开始就正确对待它。
如果你对将搜索参数作为一等状态的可能性感到好奇,我们邀请你尝试 TanStack Router。体验在路由逻辑中验证、可推断和响应性搜索参数的力量。
最后
搜索参数很可能导致类型安全和协调等问题。TanStack Router 提供了一种解决方案,将搜索参数模式整合到路由定义中,实现验证、推断和响应性,从而防止模式碎片化并确保安全的分层验证!
原文链接:tanstack.com/blog/search...