search-params
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSearch Params
搜索参数
TanStack Router treats search params as JSON-first application state. They are automatically parsed from the URL into structured objects (numbers, booleans, arrays, nested objects) and validated via on each route.
validateSearchCRITICAL: When usingand Zod v3, usezodValidator()fromfallback(), NOT zod's@tanstack/zod-adapter. Using.catch()with the zod adapter makes the output type.catch(), destroying type safety. This does not apply to Valibot or ArkType (which use their own fallback mechanisms). It also does not apply to Zod v4, which should useunknownand not use the.catch(). CRITICAL: Types are fully inferred. Never annotate the return ofzodValidator().useSearch()
TanStack Router 将搜索参数视为 JSON 优先的应用状态。它们会自动从 URL 解析为结构化对象(数字、布尔值、数组、嵌套对象),并通过每个路由的 进行校验。
validateSearch关键提示:使用和 Zod v3 时,请使用zodValidator()提供的@tanstack/zod-adapter,切勿使用 Zod 自带的fallback()。在 Zod 适配器中使用.catch()会导致输出类型变为.catch(),破坏类型安全。这一限制不适用于 Valibot 或 ArkType(它们有自身的 fallback 机制),也不适用于 Zod v4——Zod v4 应使用unknown而非.catch()。 关键提示:类型是完全自动推断的。切勿为zodValidator()的返回值添加类型注解。useSearch()
Setup: Zod Adapter (Recommended)
配置:Zod 适配器(推荐)
bash
npm install zod @tanstack/zod-adaptertsx
// 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>
)
}bash
npm install zod @tanstack/zod-adaptertsx
// 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>
)
}Reading Search Params
读取搜索参数
In Route Components: Route.useSearch()
Route.useSearch()在路由组件中:Route.useSearch()
Route.useSearch()tsx
function ProductsPage() {
const { page, sort } = Route.useSearch()
return <div>Page {page}</div>
}tsx
function ProductsPage() {
const { page, sort } = Route.useSearch()
return <div>Page {page}</div>
}In Code-Split Components: getRouteApi()
getRouteApi()在代码分割组件中:getRouteApi()
getRouteApi()tsx
import { getRouteApi } from '@tanstack/react-router'
const routeApi = getRouteApi('/products')
function ProductFilters() {
const { sort } = routeApi.useSearch()
return <select value={sort}>{/* options */}</select>
}tsx
import { getRouteApi } from '@tanstack/react-router'
const routeApi = getRouteApi('/products')
function ProductFilters() {
const { sort } = routeApi.useSearch()
return <select value={sort}>{/* options */}</select>
}From Any Component: useSearch({ from })
useSearch({ from })从任意组件读取:useSearch({ from })
useSearch({ from })tsx
import { useSearch } from '@tanstack/react-router'
function SortIndicator() {
const { sort } = useSearch({ from: '/products' })
return <span>Sorted by: {sort}</span>
}tsx
import { useSearch } from '@tanstack/react-router'
function SortIndicator() {
const { sort } = useSearch({ from: '/products' })
return <span>Sorted by: {sort}</span>
}Loose Access: useSearch({ strict: false })
useSearch({ strict: false })宽松访问:useSearch({ strict: false })
useSearch({ strict: false })tsx
function GenericPaginator() {
const search = useSearch({ strict: false })
// search.page is number | undefined (union of all routes)
return <span>Page: {search.page ?? 1}</span>
}tsx
function GenericPaginator() {
const search = useSearch({ strict: false })
// search.page is number | undefined (union of all routes)
return <span>Page: {search.page ?? 1}</span>
}Writing Search Params
写入搜索参数
Link with Function Form (Preserves Existing Params)
函数形式的 Link(保留现有参数)
tsx
import { Link } from '@tanstack/react-router'
function Pagination() {
return (
<Link
from="/products"
search={(prev) => ({ ...prev, page: prev.page + 1 })}
>
Next Page
</Link>
)
}tsx
import { Link } from '@tanstack/react-router'
function Pagination() {
return (
<Link
from="/products"
search={(prev) => ({ ...prev, page: prev.page + 1 })}
>
Next Page
</Link>
)
}Link with Object Form (Replaces All Params)
对象形式的 Link(替换所有参数)
tsx
<Link to="/products" search={{ page: 1, filter: '', sort: 'newest' }}>
Reset
</Link>tsx
<Link to="/products" search={{ page: 1, filter: '', sort: 'newest' }}>
Reset
</Link>Programmatic: useNavigate()
useNavigate()编程式导航:useNavigate()
useNavigate()tsx
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>
)
}tsx
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>
)
}Search Param Inheritance
搜索参数继承
Parent route search params are automatically merged into child routes:
tsx
// 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,
})tsx
// 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>
}父路由的搜索参数会自动合并到子路由中:
tsx
// 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,
})tsx
// 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>
}Search Middlewares
搜索中间件
retainSearchParams
— Keep Params Across Navigation
retainSearchParamsretainSearchParams
— 跨导航保留参数
retainSearchParamstsx
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'])],
},
})tsx
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
— Remove Default Values from URL
stripSearchParamsstripSearchParams
— 从 URL 中移除默认值
stripSearchParamstsx
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)],
},
})tsx
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)],
},
})Chaining Middlewares
中间件链式调用
tsx
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'] }),
],
},
})tsx
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'] }),
],
},
})Custom Serialization
自定义序列化
Override the default JSON serialization at the router level:
tsx
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),
})在路由器层面覆盖默认的 JSON 序列化:
tsx
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),
})Using Search Params in Loaders via loaderDeps
loaderDeps通过 loaderDeps
在加载器中使用搜索参数
loaderDepstsx
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 })
},
})tsx
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 })
},
})Common Mistakes
常见错误
1. HIGH: Using zod v3's .catch()
with zodValidator()
instead of adapter fallback()
.catch()zodValidator()fallback()1. 高风险:在 Zod v3 中搭配 zodValidator()
使用 .catch()
而非适配器的 fallback()
zodValidator().catch()fallback()tsx
// 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) })Important: This only applies when using Zod v3, not when using Zod v4. For v4, using is correct.
.catch()tsx
// 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: Returning entire search object from loaderDeps
loaderDeps2. 高风险:从 loaderDeps
返回整个搜索对象
loaderDepstsx
// 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 })tsx
// 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: Passing Date objects in search params
3. 高风险:在搜索参数中传递 Date 对象
tsx
// 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() }}>tsx
// 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: Parent route missing validateSearch
blocks inheritance
validateSearch4. 中风险:父路由缺少 validateSearch
导致继承失效
validateSearchtsx
// 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,
})tsx
// 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 (cross-skill): Using search as object instead of function loses params
5. 高风险(跨技能点):使用对象形式而非函数形式的 search 导致参数丢失
tsx
// 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>tsx
// 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>References
参考资料
- Validation Patterns Reference — comprehensive patterns for all validation libraries
- 校验模式参考 — 涵盖所有校验库的全面模式