SKILL.md
SKILL.mdBrowse 2 files
2,306 tokens
9,210 bytes
Token encoding: o200k_base
Snapshot 8e164d2
1---2name: search-params3description: >-4 validateSearch, search param validation with Zod/Valibot/ArkType adapters,5 fallback(), search middlewares (retainSearchParams, stripSearchParams),6 custom serialization (parseSearch, stringifySearch), search param7 inheritance, loaderDeps for cache keys, reading and writing search params.8metadata:9 type: sub-skill10 library: tanstack-router11 library_version: '1.171.15'12requires:13 - router-core14sources:15 - TanStack/router:docs/router/guide/search-params.md16 - TanStack/router:docs/router/how-to/setup-basic-search-params.md17 - TanStack/router:docs/router/how-to/validate-search-params.md18 - TanStack/router:docs/router/how-to/navigate-with-search-params.md19 - TanStack/router:docs/router/how-to/share-search-params-across-routes.md20 - TanStack/router:docs/router/guide/custom-search-param-serialization.md21---22 23# Search Params24 25TanStack 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.26 27> **CRITICAL**: When using `zodValidator()` and Zod v3, 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). It also does not apply to Zod v4, which should use `.catch()` and not use the `zodValidator()`.28> **CRITICAL**: Types are fully inferred. Never annotate the return of `useSearch()`.29 30## Setup: Zod Adapter (Recommended)31 32```bash33npm install zod @tanstack/zod-adapter34```35 36```tsx37// src/routes/products.tsx38import { createFileRoute } from '@tanstack/react-router'39import { z } from 'zod'40 41const productSearchSchema = z.object({42 page: z.number().default(1).catch(1),43 filter: z.string().default(''),44 sort: z.enum(['newest', 'oldest', 'price']).default('newest').catch('newest'),45})46 47export const Route = createFileRoute('/products')({48 validateSearch: productSearchSchema,49 component: ProductsPage,50})51 52function ProductsPage() {53 // page: number, filter: string, sort: 'newest' | 'oldest' | 'price'54 // ALL INFERRED — do not annotate55 const { page, filter, sort } = Route.useSearch()56 57 return (58 <div>59 <p>60 Page {page}, filter: {filter}, sort: {sort}61 </p>62 </div>63 )64}65```66 67## Reading Search Params68 69### In Route Components: `Route.useSearch()`70 71```tsx72function ProductsPage() {73 const { page, sort } = Route.useSearch()74 return <div>Page {page}</div>75}76```77 78### In Code-Split Components: `getRouteApi()`79 80```tsx81import { getRouteApi } from '@tanstack/react-router'82 83const routeApi = getRouteApi('/products')84 85function ProductFilters() {86 const { sort } = routeApi.useSearch()87 return <select value={sort}>{/* options */}</select>88}89```90 91### From Any Component: `useSearch({ from })`92 93```tsx94import { useSearch } from '@tanstack/react-router'95 96function SortIndicator() {97 const { sort } = useSearch({ from: '/products' })98 return <span>Sorted by: {sort}</span>99}100```101 102### Loose Access: `useSearch({ strict: false })`103 104```tsx105function GenericPaginator() {106 const search = useSearch({ strict: false })107 // search.page is number | undefined (union of all routes)108 return <span>Page: {search.page ?? 1}</span>109}110```111 112## Writing Search Params113 114### Link with Function Form (Preserves Existing Params)115 116```tsx117import { Link } from '@tanstack/react-router'118 119function Pagination() {120 return (121 <Link122 from="/products"123 search={(prev) => ({ ...prev, page: prev.page + 1 })}124 >125 Next Page126 </Link>127 )128}129```130 131### Link with Object Form (Replaces All Params)132 133```tsx134<Link to="/products" search={{ page: 1, filter: '', sort: 'newest' }}>135 Reset136</Link>137```138 139### Programmatic: `useNavigate()`140 141```tsx142import { useNavigate } from '@tanstack/react-router'143 144function SortDropdown() {145 const navigate = useNavigate({ from: '/products' })146 147 return (148 <select149 onChange={(e) => {150 navigate({151 search: (prev) => ({ ...prev, sort: e.target.value, page: 1 }),152 })153 }}154 >155 <option value="newest">Newest</option>156 <option value="price">Price</option>157 </select>158 )159}160```161 162## Search Param Inheritance163 164Parent route search params are automatically merged into child routes:165 166```tsx167// src/routes/shop.tsx — parent defines shared params168import { createFileRoute } from '@tanstack/react-router'169import { z } from 'zod'170 171const shopSearchSchema = z.object({172 currency: z.enum(['USD', 'EUR']).default('USD').catch('USD'),173})174 175export const Route = createFileRoute('/shop')({176 validateSearch: shopSearchSchema,177})178```179 180```tsx181// src/routes/shop/products.tsx — child inherits currency182import { createFileRoute } from '@tanstack/react-router'183 184export const Route = createFileRoute('/shop/products')({185 component: ShopProducts,186})187 188function ShopProducts() {189 // currency is available here from parent — fully typed190 const { currency } = Route.useSearch()191 return <div>Currency: {currency}</div>192}193```194 195## Search Middlewares196 197### `retainSearchParams` — Keep Params Across Navigation198 199```tsx200import { createRootRoute, retainSearchParams } from '@tanstack/react-router'201import { z } from 'zod'202 203const rootSearchSchema = z.object({204 debug: z.boolean().optional(),205})206 207export const Route = createRootRoute({208 validateSearch: rootSearchSchema,209 search: {210 middlewares: [retainSearchParams(['debug'])],211 },212})213```214 215### `stripSearchParams` — Remove Default Values from URL216 217```tsx218import { createFileRoute, stripSearchParams } from '@tanstack/react-router'219import { z } from 'zod'220 221const defaults = { sort: 'newest', page: 1 }222 223const searchSchema = z.object({224 sort: z.string().default(defaults.sort),225 page: z.number().default(defaults.page),226})227 228export const Route = createFileRoute('/items')({229 validateSearch: searchSchema,230 search: {231 middlewares: [stripSearchParams(defaults)],232 },233})234```235 236### Chaining Middlewares237 238```tsx239export const Route = createFileRoute('/search')({240 validateSearch: z.object({241 retainMe: z.string().optional(),242 arrayWithDefaults: z.string().array().default(['foo', 'bar']),243 required: z.string(),244 }),245 search: {246 middlewares: [247 retainSearchParams(['retainMe']),248 stripSearchParams({ arrayWithDefaults: ['foo', 'bar'] }),249 ],250 },251})252```253 254## Custom Serialization255 256Override the default JSON serialization at the router level:257 258```tsx259import {260 createRouter,261 parseSearchWith,262 stringifySearchWith,263} from '@tanstack/react-router'264 265const router = createRouter({266 routeTree,267 // Example: use JSURL2 for compact, human-readable URLs268 parseSearch: parseSearchWith(parse),269 stringifySearch: stringifySearchWith(stringify),270})271```272 273## Using Search Params in Loaders via `loaderDeps`274 275```tsx276export const Route = createFileRoute('/products')({277 validateSearch: productSearchSchema,278 // Pick ONLY the params the loader needs — not the entire search object279 loaderDeps: ({ search }) => ({ page: search.page }),280 loader: async ({ deps }) => {281 return fetchProducts({ page: deps.page })282 },283})284```285 286## Common Mistakes287 288### 1. HIGH: Using zod v3's `.catch()` with `zodValidator()` instead of adapter `fallback()`289 290```tsx291// WRONG — .catch() with zodValidator makes the type unknown292const schema = z.object({ page: z.number().catch(1) })293validateSearch: zodValidator(schema) // page is typed as unknown!294 295// CORRECT — fallback() preserves the inferred type296import { fallback } from '@tanstack/zod-adapter'297const schema = z.object({ page: fallback(z.number(), 1) })298```299 300**Important:** This only applies when using Zod v3, not when using Zod v4. For v4, using `.catch()` is correct.301 302### 2. HIGH: Returning entire search object from `loaderDeps`303 304```tsx305// WRONG — loader re-runs on ANY search param change306loaderDeps: ({ search }) => search307 308// CORRECT — loader only re-runs when page changes309loaderDeps: ({ search }) => ({ page: search.page })310```311 312### 3. HIGH: Passing Date objects in search params313 314```tsx315// WRONG — Date does not serialize correctly to JSON in URLs316<Link search={{ startDate: new Date() }}>317 318// CORRECT — convert to ISO string319<Link search={{ startDate: new Date().toISOString() }}>320```321 322### 4. MEDIUM: Parent route missing `validateSearch` blocks inheritance323 324```tsx325// WRONG — child cannot access shared params326export const Route = createRootRoute({327 component: RootComponent,328 // no validateSearch!329})330 331// CORRECT — parent must define validateSearch for children to inherit332export const Route = createRootRoute({333 validateSearch: globalSearchSchema,334 component: RootComponent,335})336```337 338### 5. HIGH (cross-skill): Using search as object instead of function loses params339 340```tsx341// WRONG — replaces ALL search params, losing any existing ones342<Link to="." search={{ page: 2 }}>Page 2</Link>343 344// CORRECT — preserves existing params, updates only page345<Link to="." search={(prev) => ({ ...prev, page: 2 })}>Page 2</Link>346```347 348## References349 350- [Validation Patterns Reference](./references/validation-patterns.md) — comprehensive patterns for all validation libraries351 Discovery context
Discovered by repository scan. No exact path reference found in the snapshot’s root AGENTS.md.