@tanstack/router-core 1.167.3 → 1.167.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,355 @@
1
+ ---
2
+ name: router-core/search-params
3
+ description: >-
4
+ validateSearch, search param validation with Zod/Valibot/ArkType adapters,
5
+ fallback(), search middlewares (retainSearchParams, stripSearchParams),
6
+ custom serialization (parseSearch, stringifySearch), search param
7
+ inheritance, loaderDeps for cache keys, reading and writing search params.
8
+ type: sub-skill
9
+ library: tanstack-router
10
+ library_version: '1.166.2'
11
+ requires:
12
+ - router-core
13
+ sources:
14
+ - TanStack/router:docs/router/guide/search-params.md
15
+ - TanStack/router:docs/router/how-to/setup-basic-search-params.md
16
+ - TanStack/router:docs/router/how-to/validate-search-params.md
17
+ - TanStack/router:docs/router/how-to/navigate-with-search-params.md
18
+ - TanStack/router:docs/router/how-to/share-search-params-across-routes.md
19
+ - TanStack/router:docs/router/guide/custom-search-param-serialization.md
20
+ ---
21
+
22
+ # Search Params
23
+
24
+ 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.
25
+
26
+ > **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).
27
+ > **CRITICAL**: Types are fully inferred. Never annotate the return of `useSearch()`.
28
+
29
+ ## Setup: Zod Adapter (Recommended)
30
+
31
+ ```bash
32
+ npm install zod @tanstack/zod-adapter
33
+ ```
34
+
35
+ ```tsx
36
+ // src/routes/products.tsx
37
+ import { createFileRoute } from '@tanstack/react-router'
38
+ import { zodValidator, fallback } from '@tanstack/zod-adapter'
39
+ import { z } from 'zod'
40
+
41
+ const productSearchSchema = z.object({
42
+ page: fallback(z.number(), 1).default(1),
43
+ filter: fallback(z.string(), '').default(''),
44
+ sort: fallback(z.enum(['newest', 'oldest', 'price']), 'newest').default(
45
+ 'newest',
46
+ ),
47
+ })
48
+
49
+ export const Route = createFileRoute('/products')({
50
+ validateSearch: zodValidator(productSearchSchema),
51
+ component: ProductsPage,
52
+ })
53
+
54
+ function ProductsPage() {
55
+ // page: number, filter: string, sort: 'newest' | 'oldest' | 'price'
56
+ // ALL INFERRED — do not annotate
57
+ const { page, filter, sort } = Route.useSearch()
58
+
59
+ return (
60
+ <div>
61
+ <p>
62
+ Page {page}, filter: {filter}, sort: {sort}
63
+ </p>
64
+ </div>
65
+ )
66
+ }
67
+ ```
68
+
69
+ ## Reading Search Params
70
+
71
+ ### In Route Components: `Route.useSearch()`
72
+
73
+ ```tsx
74
+ function ProductsPage() {
75
+ const { page, sort } = Route.useSearch()
76
+ return <div>Page {page}</div>
77
+ }
78
+ ```
79
+
80
+ ### In Code-Split Components: `getRouteApi()`
81
+
82
+ ```tsx
83
+ import { getRouteApi } from '@tanstack/react-router'
84
+
85
+ const routeApi = getRouteApi('/products')
86
+
87
+ function ProductFilters() {
88
+ const { sort } = routeApi.useSearch()
89
+ return <select value={sort}>{/* options */}</select>
90
+ }
91
+ ```
92
+
93
+ ### From Any Component: `useSearch({ from })`
94
+
95
+ ```tsx
96
+ import { useSearch } from '@tanstack/react-router'
97
+
98
+ function SortIndicator() {
99
+ const { sort } = useSearch({ from: '/products' })
100
+ return <span>Sorted by: {sort}</span>
101
+ }
102
+ ```
103
+
104
+ ### Loose Access: `useSearch({ strict: false })`
105
+
106
+ ```tsx
107
+ function GenericPaginator() {
108
+ const search = useSearch({ strict: false })
109
+ // search.page is number | undefined (union of all routes)
110
+ return <span>Page: {search.page ?? 1}</span>
111
+ }
112
+ ```
113
+
114
+ ## Writing Search Params
115
+
116
+ ### Link with Function Form (Preserves Existing Params)
117
+
118
+ ```tsx
119
+ import { Link } from '@tanstack/react-router'
120
+
121
+ function Pagination() {
122
+ return (
123
+ <Link
124
+ from="/products"
125
+ search={(prev) => ({ ...prev, page: prev.page + 1 })}
126
+ >
127
+ Next Page
128
+ </Link>
129
+ )
130
+ }
131
+ ```
132
+
133
+ ### Link with Object Form (Replaces All Params)
134
+
135
+ ```tsx
136
+ <Link to="/products" search={{ page: 1, filter: '', sort: 'newest' }}>
137
+ Reset
138
+ </Link>
139
+ ```
140
+
141
+ ### Programmatic: `useNavigate()`
142
+
143
+ ```tsx
144
+ import { useNavigate } from '@tanstack/react-router'
145
+
146
+ function SortDropdown() {
147
+ const navigate = useNavigate({ from: '/products' })
148
+
149
+ return (
150
+ <select
151
+ onChange={(e) => {
152
+ navigate({
153
+ search: (prev) => ({ ...prev, sort: e.target.value, page: 1 }),
154
+ })
155
+ }}
156
+ >
157
+ <option value="newest">Newest</option>
158
+ <option value="price">Price</option>
159
+ </select>
160
+ )
161
+ }
162
+ ```
163
+
164
+ ## Search Param Inheritance
165
+
166
+ Parent route search params are automatically merged into child routes:
167
+
168
+ ```tsx
169
+ // src/routes/shop.tsx — parent defines shared params
170
+ import { createFileRoute } from '@tanstack/react-router'
171
+ import { zodValidator, fallback } from '@tanstack/zod-adapter'
172
+ import { z } from 'zod'
173
+
174
+ const shopSearchSchema = z.object({
175
+ currency: fallback(z.enum(['USD', 'EUR']), 'USD').default('USD'),
176
+ })
177
+
178
+ export const Route = createFileRoute('/shop')({
179
+ validateSearch: zodValidator(shopSearchSchema),
180
+ })
181
+ ```
182
+
183
+ ```tsx
184
+ // src/routes/shop/products.tsx — child inherits currency
185
+ import { createFileRoute } from '@tanstack/react-router'
186
+
187
+ export const Route = createFileRoute('/shop/products')({
188
+ component: ShopProducts,
189
+ })
190
+
191
+ function ShopProducts() {
192
+ // currency is available here from parent — fully typed
193
+ const { currency } = Route.useSearch()
194
+ return <div>Currency: {currency}</div>
195
+ }
196
+ ```
197
+
198
+ ## Search Middlewares
199
+
200
+ ### `retainSearchParams` — Keep Params Across Navigation
201
+
202
+ ```tsx
203
+ import { createRootRoute, retainSearchParams } from '@tanstack/react-router'
204
+ import { zodValidator } from '@tanstack/zod-adapter'
205
+ import { z } from 'zod'
206
+
207
+ const rootSearchSchema = z.object({
208
+ debug: z.boolean().optional(),
209
+ })
210
+
211
+ export const Route = createRootRoute({
212
+ validateSearch: zodValidator(rootSearchSchema),
213
+ search: {
214
+ middlewares: [retainSearchParams(['debug'])],
215
+ },
216
+ })
217
+ ```
218
+
219
+ ### `stripSearchParams` — Remove Default Values from URL
220
+
221
+ ```tsx
222
+ import { createFileRoute, stripSearchParams } from '@tanstack/react-router'
223
+ import { zodValidator } from '@tanstack/zod-adapter'
224
+ import { z } from 'zod'
225
+
226
+ const defaults = { sort: 'newest', page: 1 }
227
+
228
+ const searchSchema = z.object({
229
+ sort: z.string().default(defaults.sort),
230
+ page: z.number().default(defaults.page),
231
+ })
232
+
233
+ export const Route = createFileRoute('/items')({
234
+ validateSearch: zodValidator(searchSchema),
235
+ search: {
236
+ middlewares: [stripSearchParams(defaults)],
237
+ },
238
+ })
239
+ ```
240
+
241
+ ### Chaining Middlewares
242
+
243
+ ```tsx
244
+ export const Route = createFileRoute('/search')({
245
+ validateSearch: zodValidator(
246
+ z.object({
247
+ retainMe: z.string().optional(),
248
+ arrayWithDefaults: z.string().array().default(['foo', 'bar']),
249
+ required: z.string(),
250
+ }),
251
+ ),
252
+ search: {
253
+ middlewares: [
254
+ retainSearchParams(['retainMe']),
255
+ stripSearchParams({ arrayWithDefaults: ['foo', 'bar'] }),
256
+ ],
257
+ },
258
+ })
259
+ ```
260
+
261
+ ## Custom Serialization
262
+
263
+ Override the default JSON serialization at the router level:
264
+
265
+ ```tsx
266
+ import {
267
+ createRouter,
268
+ parseSearchWith,
269
+ stringifySearchWith,
270
+ } from '@tanstack/react-router'
271
+
272
+ const router = createRouter({
273
+ routeTree,
274
+ // Example: use JSURL2 for compact, human-readable URLs
275
+ parseSearch: parseSearchWith(parse),
276
+ stringifySearch: stringifySearchWith(stringify),
277
+ })
278
+ ```
279
+
280
+ ## Using Search Params in Loaders via `loaderDeps`
281
+
282
+ ```tsx
283
+ export const Route = createFileRoute('/products')({
284
+ validateSearch: zodValidator(productSearchSchema),
285
+ // Pick ONLY the params the loader needs — not the entire search object
286
+ loaderDeps: ({ search }) => ({ page: search.page }),
287
+ loader: async ({ deps }) => {
288
+ return fetchProducts({ page: deps.page })
289
+ },
290
+ })
291
+ ```
292
+
293
+ ## Common Mistakes
294
+
295
+ ### 1. HIGH: Using zod `.catch()` with `zodValidator()` instead of adapter `fallback()`
296
+
297
+ ```tsx
298
+ // WRONG — .catch() with zodValidator makes the type unknown
299
+ const schema = z.object({ page: z.number().catch(1) })
300
+ validateSearch: zodValidator(schema) // page is typed as unknown!
301
+
302
+ // CORRECT — fallback() preserves the inferred type
303
+ import { fallback } from '@tanstack/zod-adapter'
304
+ const schema = z.object({ page: fallback(z.number(), 1) })
305
+ ```
306
+
307
+ ### 2. HIGH: Returning entire search object from `loaderDeps`
308
+
309
+ ```tsx
310
+ // WRONG — loader re-runs on ANY search param change
311
+ loaderDeps: ({ search }) => search
312
+
313
+ // CORRECT — loader only re-runs when page changes
314
+ loaderDeps: ({ search }) => ({ page: search.page })
315
+ ```
316
+
317
+ ### 3. HIGH: Passing Date objects in search params
318
+
319
+ ```tsx
320
+ // WRONG — Date does not serialize correctly to JSON in URLs
321
+ <Link search={{ startDate: new Date() }}>
322
+
323
+ // CORRECT — convert to ISO string
324
+ <Link search={{ startDate: new Date().toISOString() }}>
325
+ ```
326
+
327
+ ### 4. MEDIUM: Parent route missing `validateSearch` blocks inheritance
328
+
329
+ ```tsx
330
+ // WRONG — child cannot access shared params
331
+ export const Route = createRootRoute({
332
+ component: RootComponent,
333
+ // no validateSearch!
334
+ })
335
+
336
+ // CORRECT — parent must define validateSearch for children to inherit
337
+ export const Route = createRootRoute({
338
+ validateSearch: zodValidator(globalSearchSchema),
339
+ component: RootComponent,
340
+ })
341
+ ```
342
+
343
+ ### 5. HIGH (cross-skill): Using search as object instead of function loses params
344
+
345
+ ```tsx
346
+ // WRONG — replaces ALL search params, losing any existing ones
347
+ <Link to="." search={{ page: 2 }}>Page 2</Link>
348
+
349
+ // CORRECT — preserves existing params, updates only page
350
+ <Link to="." search={(prev) => ({ ...prev, page: 2 })}>Page 2</Link>
351
+ ```
352
+
353
+ ## References
354
+
355
+ - [Validation Patterns Reference](./references/validation-patterns.md) — comprehensive patterns for all validation libraries