关于 search-params
介绍 TanStack Router 如何将 URL 查询参数作为经过验证的结构化应用状态。内容涵盖类型推断、参数读取与更新、继承、中间件和加载器依赖。
主要功能
- 01将查询参数解析为对象
- 02验证参数并推断类型
- 03在路由中读取和更新参数
- 04通过中间件保留或移除参数
- 05选择加载器依赖项
适用场景
- 01验证 URL 中的分页和排序值
- 02翻页时保留已有筛选条件
- 03在父子路由间共享查询参数
- 04仅因相关参数变化而重新运行加载器
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ tanstack/ router --skill search-params安装后在 Claude Code 中输入/search-params确认已加载。
附带文件references/validation-patterns.md
详细介绍
搜索参数
TanStack Router 将搜索参数视为 JSON 优先的应用状态。它会自动将 URL 中的参数解析为结构化对象(数字、布尔值、数组、嵌套对象),并在每个路由上通过 validateSearch 进行验证。
CRITICAL:使用
zodValidator()和 Zod v3 时,请使用@tanstack/zod-adapter中的fallback(),而不是 Zod 的.catch()。在 Zod 适配器中使用.catch()会使输出类型变为unknown,破坏类型安全。这不适用于 Valibot 或 ArkType(它们使用各自的 fallback 机制)。这也不适用于 Zod v4;Zod v4 应使用.catch(),而不应使用zodValidator()。 CRITICAL:类型会被完整推断。切勿为useSearch()的返回值添加类型注解。
设置:Zod 适配器(推荐)
npm install zod @tanstack/zod-adapter
// src/routes/products.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
const productSearchSchema = z.object({
page: z.number().default(1).catch(1),
filter: z.string().default(''),
sort: z.enum(['newest', 'oldest', 'price']).default('newest').catch('newest'),
})
export const Route = createFileRoute('/products')({
validateSearch: productSearchSchema,
component: ProductsPage,
})
function ProductsPage() {
// page: number, filter: string, sort: 'newest' | 'oldest' | 'price'
// ALL INFERRED — do not annotate
const { page, filter, sort } = Route.useSearch()
return (
<div>
<p>
Page {page}, filter: {filter}, sort: {sort}
</p>
</div>
)
}
读取搜索参数
在路由组件中:Route.useSearch()
function ProductsPage() {
const { page, sort } = Route.useSearch()
return <div>Page {page}</div>
}
在代码拆分的组件中:getRouteApi()
import { getRouteApi } from '@tanstack/react-router'
const routeApi = getRouteApi('/products')
function ProductFilters() {
const { sort } = routeApi.useSearch()
return <select value={sort}>{/* options */}</select>
}
在任意组件中:useSearch({ from })
import { useSearch } from '@tanstack/react-router'
function SortIndicator() {
const { sort } = useSearch({ from: '/products' })
return <span>Sorted by: {sort}</span>
}
宽松访问:useSearch({ strict: false })
function GenericPaginator() {
const search = useSearch({ strict: false })
// search.page is number | undefined (union of all routes)
return <span>Page: {search.page ?? 1}</span>
}
写入搜索参数
使用函数形式的 Link(保留现有参数)
import { Link } from '@tanstack/react-router'
function Pagination() {
return (
<Link
from="/products"
search={(prev) => ({ ...prev, page: prev.page + 1 })}
>
Next Page
</Link>
)
}
使用对象形式的 Link(替换所有参数)
<Link to="/products" search={{ page: 1, filter: '', sort: 'newest' }}>
Reset
</Link>
编程方式:useNavigate()
import { useNavigate } from '@tanstack/react-router'
function SortDropdown() {
const navigate = useNavigate({ from: '/products' })
return (
<select
onChange={(e) => {
navigate({
search: (prev) => ({ ...prev, sort: e.target.value, page: 1 }),
})
}}
>
<option value="newest">Newest</option>
<option value="price">Price</option>
</select>
)
}
搜索参数继承
父路由的搜索参数会自动合并到子路由中:
// src/routes/shop.tsx — parent defines shared params
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
const shopSearchSchema = z.object({
currency: z.enum(['USD', 'EUR']).default('USD').catch('USD'),
})
export const Route = createFileRoute('/shop')({
validateSearch: shopSearchSchema,
})
// src/routes/shop/products.tsx — child inherits currency
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/shop/products')({
component: ShopProducts,
})
function ShopProducts() {
// currency is available here from parent — fully typed
const { currency } = Route.useSearch()
return <div>Currency: {currency}</div>
}
搜索中间件
retainSearchParams — 在导航过程中保留参数
import { createRootRoute, retainSearchParams } from '@tanstack/react-router'
import { z } from 'zod'
const rootSearchSchema = z.object({
debug: z.boolean().optional(),
})
export const Route = createRootRoute({
validateSearch: rootSearchSchema,
search: {
middlewares: [retainSearchParams(['debug'])],
},
})
stripSearchParams — 从 URL 中移除默认值
import { createFileRoute, stripSearchParams } from '@tanstack/react-router'
import { z } from 'zod'
const defaults = { sort: 'newest', page: 1 }
const searchSchema = z.object({
sort: z.string().default(defaults.sort),
page: z.number().default(defaults.page),
})
export const Route = createFileRoute('/items')({
validateSearch: searchSchema,
search: {
middlewares: [stripSearchParams(defaults)],
},
})
链接多个中间件
export const Route = createFileRoute('/search')({
validateSearch: z.object({
retainMe: z.string().optional(),
arrayWithDefaults: z.string().array().default(['foo', 'bar']),
required: z.string(),
}),
search: {
middlewares: [
retainSearchParams(['retainMe']),
stripSearchParams({ arrayWithDefaults: ['foo', 'bar'] }),
],
},
})
自定义序列化
在路由器级别覆盖默认的 JSON 序列化:
import {
createRouter,
parseSearchWith,
stringifySearchWith,
} from '@tanstack/react-router'
const router = createRouter({
routeTree,
// Example: use JSURL2 for compact, human-readable URLs
parseSearch: parseSearchWith(parse),
stringifySearch: stringifySearchWith(stringify),
})
通过 loaderDeps 在 Loader 中使用搜索参数
export const Route = createFileRoute('/products')({
validateSearch: productSearchSchema,
// Pick ONLY the params the loader needs — not the entire search object
loaderDeps: ({ search }) => ({ page: search.page }),
loader: async ({ deps }) => {
return fetchProducts({ page: deps.page })
},
})
常见错误
1. HIGH:在 zodValidator() 中使用 Zod v3 的 .catch(),而不是适配器的 fallback()
// WRONG — .catch() with zodValidator makes the type unknown
const schema = z.object({ page: z.number().catch(1) })
validateSearch: zodValidator(schema) // page is typed as unknown!
// CORRECT — fallback() preserves the inferred type
import { fallback } from '@tanstack/zod-adapter'
const schema = z.object({ page: fallback(z.number(), 1) })
**重要提示:**这仅适用于使用 Zod v3 的情况,不适用于 Zod v4。对于 v4,使用 .catch() 才是正确的。
2. HIGH:从 loaderDeps 返回整个搜索对象
// WRONG — loader re-runs on ANY search param change
loaderDeps: ({ search }) => search
// CORRECT — loader only re-runs when page changes
loaderDeps: ({ search }) => ({ page: search.page })
3. HIGH:在搜索参数中传递 Date 对象
// WRONG — Date does not serialize correctly to JSON in URLs
<Link search={{ startDate: new Date() }}>
// CORRECT — convert to ISO string
<Link search={{ startDate: new Date().toISOString() }}>
4. MEDIUM:父路由缺少 validateSearch 会阻止继承
// WRONG — child cannot access shared params
export const Route = createRootRoute({
component: RootComponent,
// no validateSearch!
})
// CORRECT — parent must define validateSearch for children to inherit
export const Route = createRootRoute({
validateSearch: globalSearchSchema,
component: RootComponent,
})
5. HIGH(跨技能):将搜索参数用作对象而不是函数会丢失参数
// WRONG — replaces ALL search params, losing any existing ones
<Link to="." search={{ page: 2 }}>Page 2</Link>
// CORRECT — preserves existing params, updates only page
<Link to="." search={(prev) => ({ ...prev, page: 2 })}>Page 2</Link>
参考资料
- 验证模式参考 — 涵盖所有验证库的完整模式
评论
支持 Markdown