router-core/search-params
sub-skill>-
Search 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 validateSearch on each route.
CRITICAL: When using zodValidator(), use fallback() from @tanstack/zod-adapter, NOT zod's .catch(). Using .catch() with the zod adapter makes the output type unknown, destroying type safety. This does not apply to Valibot or ArkType (which use their own fallback mechanisms). CRITICAL: Types are fully inferred. Never annotate the return of useSearch().
Setup: Zod Adapter (Recommended)
npm install zod @tanstack/zod-adapter
// src/routes/products.tsx
import { createFileRoute } from '@tanstack/react-router'
import { zodValidator, fallback } from '@tanstack/zod-adapter'
import { z } from 'zod'
const productSearchSchema = z.object({
page: fallback(z.number(), 1).default(1),
filter: fallback(z.string(), '').default(''),
sort: fallback(z.enum(['newest', 'oldest', 'price']), 'newest').default(
'newest',
),
})
export const Route = createFileRoute('/products')({
validateSearch: zodValidator(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()
function ProductsPage() {
const { page, sort } = Route.useSearch()
return <div>Page {page}</div>
}
In Code-Split Components: getRouteApi()
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 })
import { useSearch } from '@tanstack/react-router'
function SortIndicator() {
const { sort } = useSearch({ from: '/products' })
return <span>Sorted by: {sort}</span>
}
Loose Access: 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>
}
Writing Search Params
Link with Function Form (Preserves Existing Params)
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 to="/products" search={{ page: 1, filter: '', sort: 'newest' }}>
Reset
</Link>
Programmatic: 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>
)
}
Search Param Inheritance
Parent route search params are automatically merged into child routes:
// src/routes/shop.tsx — parent defines shared params
import { createFileRoute } from '@tanstack/react-router'
import { zodValidator, fallback } from '@tanstack/zod-adapter'
import { z } from 'zod'
const shopSearchSchema = z.object({
currency: fallback(z.enum(['USD', 'EUR']), 'USD').default('USD'),
})
export const Route = createFileRoute('/shop')({
validateSearch: zodValidator(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>
}
Search Middlewares
retainSearchParams — Keep Params Across Navigation
import { createRootRoute, retainSearchParams } from '@tanstack/react-router'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const rootSearchSchema = z.object({
debug: z.boolean().optional(),
})
export const Route = createRootRoute({
validateSearch: zodValidator(rootSearchSchema),
search: {
middlewares: [retainSearchParams(['debug'])],
},
})
stripSearchParams — Remove Default Values from URL
import { createFileRoute, stripSearchParams } from '@tanstack/react-router'
import { zodValidator } from '@tanstack/zod-adapter'
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: zodValidator(searchSchema),
search: {
middlewares: [stripSearchParams(defaults)],
},
})
Chaining Middlewares
export const Route = createFileRoute('/search')({
validateSearch: zodValidator(
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:
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
export const Route = createFileRoute('/products')({
validateSearch: zodValidator(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 .catch() with zodValidator() instead of adapter 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) })
2. HIGH: Returning entire search object from 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: Passing Date objects in search params
// 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
// 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: zodValidator(globalSearchSchema),
component: RootComponent,
})
5. HIGH (cross-skill): Using search as object instead of function loses params
// 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