@tanstack/router-core 1.167.2 → 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,379 @@
1
+ # Search Param Validation Patterns Reference
2
+
3
+ Comprehensive validation patterns for TanStack Router search params across all supported validation approaches.
4
+
5
+ ## Zod with `@tanstack/zod-adapter`
6
+
7
+ Zod v3 does not implement Standard Schema, so the `@tanstack/zod-adapter` wrapper is required. Always use `fallback()` from the adapter instead of zod's `.catch()`. Always wrap with `zodValidator()`.
8
+
9
+ ### Basic Types
10
+
11
+ ```tsx
12
+ import { zodValidator, fallback } from '@tanstack/zod-adapter'
13
+ import { z } from 'zod'
14
+
15
+ const schema = z.object({
16
+ count: fallback(z.number(), 0),
17
+ name: fallback(z.string(), ''),
18
+ active: fallback(z.boolean(), true),
19
+ })
20
+
21
+ export const Route = createFileRoute('/example')({
22
+ validateSearch: zodValidator(schema),
23
+ })
24
+ ```
25
+
26
+ ### Optional Params
27
+
28
+ ```tsx
29
+ const schema = z.object({
30
+ // Truly optional — can be undefined in component
31
+ searchTerm: z.string().optional(),
32
+ // Optional in URL but always has a value in component
33
+ page: fallback(z.number(), 1).default(1),
34
+ })
35
+ ```
36
+
37
+ ### Default Values
38
+
39
+ ```tsx
40
+ const schema = z.object({
41
+ // .default() means the param is optional during navigation
42
+ // but always present (with default) when reading
43
+ page: fallback(z.number(), 1).default(1),
44
+ sort: fallback(z.enum(['name', 'date', 'price']), 'name').default('name'),
45
+ ascending: fallback(z.boolean(), true).default(true),
46
+ })
47
+ ```
48
+
49
+ ### Array Params
50
+
51
+ ```tsx
52
+ const schema = z.object({
53
+ tags: fallback(z.string().array(), []).default([]),
54
+ selectedIds: fallback(z.number().array(), []).default([]),
55
+ })
56
+
57
+ // URL: /items?tags=%5B%22react%22%2C%22typescript%22%5D&selectedIds=%5B1%2C2%2C3%5D
58
+ // Parsed: { tags: ['react', 'typescript'], selectedIds: [1, 2, 3] }
59
+ ```
60
+
61
+ ### Nested Object Params
62
+
63
+ ```tsx
64
+ const schema = z.object({
65
+ filters: fallback(
66
+ z.object({
67
+ status: z.enum(['active', 'inactive']).optional(),
68
+ tags: z.string().array().optional(),
69
+ priceRange: z
70
+ .object({
71
+ min: z.number().min(0),
72
+ max: z.number().min(0),
73
+ })
74
+ .optional(),
75
+ }),
76
+ {},
77
+ ).default({}),
78
+ })
79
+ ```
80
+
81
+ ### Enum with Constraints
82
+
83
+ ```tsx
84
+ const schema = z.object({
85
+ sort: fallback(z.enum(['newest', 'oldest', 'price']), 'newest').default(
86
+ 'newest',
87
+ ),
88
+ page: fallback(z.number().int().min(1).max(1000), 1).default(1),
89
+ limit: fallback(z.number().int().min(10).max(100), 20).default(20),
90
+ })
91
+ ```
92
+
93
+ ### Discriminated Union
94
+
95
+ ```tsx
96
+ const schema = z.object({
97
+ searchType: fallback(z.enum(['basic', 'advanced']), 'basic').default('basic'),
98
+ query: fallback(z.string(), '').default(''),
99
+ // Advanced-only fields are optional
100
+ category: z.string().optional(),
101
+ minPrice: z.number().optional(),
102
+ maxPrice: z.number().optional(),
103
+ })
104
+ ```
105
+
106
+ For true discriminated union validation:
107
+
108
+ ```tsx
109
+ const basicSearch = z.object({
110
+ searchType: z.literal('basic'),
111
+ query: z.string(),
112
+ })
113
+
114
+ const advancedSearch = z.object({
115
+ searchType: z.literal('advanced'),
116
+ query: z.string(),
117
+ category: z.string(),
118
+ minPrice: z.number(),
119
+ maxPrice: z.number(),
120
+ })
121
+
122
+ const schema = z.discriminatedUnion('searchType', [basicSearch, advancedSearch])
123
+
124
+ export const Route = createFileRoute('/search')({
125
+ validateSearch: zodValidator(schema),
126
+ })
127
+ ```
128
+
129
+ ### Input Transforms (String to Number)
130
+
131
+ When using the zod adapter with transforms, configure `input` and `output` types:
132
+
133
+ ```tsx
134
+ const schema = z.object({
135
+ page: fallback(z.number(), 1).default(1),
136
+ filter: fallback(z.string(), '').default(''),
137
+ })
138
+
139
+ export const Route = createFileRoute('/items')({
140
+ // Default: input type used for navigation, output type used for reading
141
+ validateSearch: zodValidator(schema),
142
+
143
+ // Advanced: swap input/output inference
144
+ // validateSearch: zodValidator({ schema, input: 'output', output: 'input' }),
145
+ })
146
+ ```
147
+
148
+ ### Schema Composition
149
+
150
+ ```tsx
151
+ const paginationSchema = z.object({
152
+ page: fallback(z.number().int().positive(), 1).default(1),
153
+ limit: fallback(z.number().int().min(1).max(100), 20).default(20),
154
+ })
155
+
156
+ const sortSchema = z.object({
157
+ sortBy: z.enum(['name', 'date', 'relevance']).optional(),
158
+ sortOrder: z.enum(['asc', 'desc']).optional(),
159
+ })
160
+
161
+ // Compose for specific routes
162
+ const productSearchSchema = paginationSchema.extend({
163
+ category: z.string().optional(),
164
+ inStock: fallback(z.boolean(), true).default(true),
165
+ })
166
+
167
+ const userSearchSchema = paginationSchema.merge(sortSchema).extend({
168
+ role: z.enum(['admin', 'user']).optional(),
169
+ })
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Valibot (Standard Schema)
175
+
176
+ Valibot 1.0+ implements Standard Schema. No adapter wrapper needed — pass the schema directly to `validateSearch`. The `@tanstack/valibot-adapter` is optional and only needed for explicit input/output type control.
177
+
178
+ ```bash
179
+ npm install valibot
180
+ ```
181
+
182
+ ```tsx
183
+ import { createFileRoute } from '@tanstack/react-router'
184
+ import * as v from 'valibot'
185
+
186
+ const productSearchSchema = v.object({
187
+ page: v.optional(v.fallback(v.number(), 1), 1),
188
+ filter: v.optional(v.fallback(v.string(), ''), ''),
189
+ sort: v.optional(
190
+ v.fallback(v.picklist(['newest', 'oldest', 'price']), 'newest'),
191
+ 'newest',
192
+ ),
193
+ })
194
+
195
+ export const Route = createFileRoute('/products')({
196
+ // Pass schema directly — Standard Schema compliant
197
+ validateSearch: productSearchSchema,
198
+ component: ProductsPage,
199
+ })
200
+
201
+ function ProductsPage() {
202
+ const { page, filter, sort } = Route.useSearch()
203
+ return <div>Page {page}</div>
204
+ }
205
+ ```
206
+
207
+ ### Valibot with Constraints
208
+
209
+ ```tsx
210
+ import * as v from 'valibot'
211
+
212
+ const schema = v.object({
213
+ page: v.optional(
214
+ v.fallback(v.pipe(v.number(), v.integer(), v.minValue(1)), 1),
215
+ 1,
216
+ ),
217
+ query: v.optional(v.pipe(v.string(), v.minLength(1), v.maxLength(100))),
218
+ tags: v.optional(v.fallback(v.array(v.string()), []), []),
219
+ })
220
+ ```
221
+
222
+ ### Valibot with Adapter (Alternative)
223
+
224
+ If you need explicit input/output type control:
225
+
226
+ ```tsx
227
+ import { valibotValidator } from '@tanstack/valibot-adapter'
228
+ import * as v from 'valibot'
229
+
230
+ const schema = v.object({
231
+ page: v.optional(v.fallback(v.number(), 1), 1),
232
+ })
233
+
234
+ export const Route = createFileRoute('/items')({
235
+ validateSearch: valibotValidator(schema),
236
+ })
237
+ ```
238
+
239
+ ---
240
+
241
+ ## ArkType (Standard Schema)
242
+
243
+ ArkType 2.0-rc+ implements Standard Schema. No adapter needed — pass the type directly to `validateSearch`. The `@tanstack/arktype-adapter` is optional and only needed for explicit input/output type control.
244
+
245
+ ```bash
246
+ npm install arktype
247
+ ```
248
+
249
+ ```tsx
250
+ import { createFileRoute } from '@tanstack/react-router'
251
+ import { type } from 'arktype'
252
+
253
+ const productSearchSchema = type({
254
+ page: 'number = 1',
255
+ filter: 'string = ""',
256
+ sort: '"newest" | "oldest" | "price" = "newest"',
257
+ })
258
+
259
+ export const Route = createFileRoute('/products')({
260
+ // Pass directly — Standard Schema compliant
261
+ validateSearch: productSearchSchema,
262
+ component: ProductsPage,
263
+ })
264
+
265
+ function ProductsPage() {
266
+ const { page, filter, sort } = Route.useSearch()
267
+ return <div>Page {page}</div>
268
+ }
269
+ ```
270
+
271
+ ### ArkType with Constraints
272
+
273
+ ```tsx
274
+ import { type } from 'arktype'
275
+
276
+ const searchSchema = type({
277
+ 'query?': 'string>0&<=100',
278
+ page: 'number>0 = 1',
279
+ 'sortBy?': "'name'|'date'|'relevance'",
280
+ 'filters?': 'string[]',
281
+ })
282
+ ```
283
+
284
+ ---
285
+
286
+ ## Manual Validation Function
287
+
288
+ For full control without any library. The function receives raw JSON-parsed (but unvalidated) search params.
289
+
290
+ ```tsx
291
+ import { createFileRoute } from '@tanstack/react-router'
292
+
293
+ type ProductSearch = {
294
+ page: number
295
+ filter: string
296
+ sort: 'newest' | 'oldest' | 'price'
297
+ }
298
+
299
+ export const Route = createFileRoute('/products')({
300
+ validateSearch: (search: Record<string, unknown>): ProductSearch => ({
301
+ page: Number(search?.page ?? 1),
302
+ filter: (search.filter as string) || '',
303
+ sort:
304
+ search.sort === 'newest' ||
305
+ search.sort === 'oldest' ||
306
+ search.sort === 'price'
307
+ ? search.sort
308
+ : 'newest',
309
+ }),
310
+ component: ProductsPage,
311
+ })
312
+
313
+ function ProductsPage() {
314
+ const { page, filter, sort } = Route.useSearch()
315
+ return <div>Page {page}</div>
316
+ }
317
+ ```
318
+
319
+ ### Manual Validation with Error Throwing
320
+
321
+ If `validateSearch` throws, the route's `errorComponent` renders instead:
322
+
323
+ ```tsx
324
+ export const Route = createFileRoute('/products')({
325
+ validateSearch: (search: Record<string, unknown>) => {
326
+ const page = Number(search.page)
327
+ if (isNaN(page) || page < 1) {
328
+ throw new Error('Invalid page number')
329
+ }
330
+ return { page }
331
+ },
332
+ errorComponent: ({ error }) => <div>Bad search params: {error.message}</div>,
333
+ })
334
+ ```
335
+
336
+ ---
337
+
338
+ ## Pattern: Object with `parse` Method
339
+
340
+ Any object with a `.parse()` method works as `validateSearch`:
341
+
342
+ ```tsx
343
+ const mySchema = {
344
+ parse: (input: Record<string, unknown>) => ({
345
+ page: Number(input.page ?? 1),
346
+ query: String(input.query ?? ''),
347
+ }),
348
+ }
349
+
350
+ export const Route = createFileRoute('/search')({
351
+ validateSearch: mySchema,
352
+ })
353
+ ```
354
+
355
+ ---
356
+
357
+ ## Dates in Search Params
358
+
359
+ Never put `Date` objects in search params. Always use ISO strings:
360
+
361
+ ```tsx
362
+ const schema = z.object({
363
+ // Store as string, parse in component if needed
364
+ startDate: z.string().optional(),
365
+ endDate: z.string().optional(),
366
+ })
367
+
368
+ // In component:
369
+ function DateFilter() {
370
+ const { startDate } = Route.useSearch()
371
+ const date = startDate ? new Date(startDate) : null
372
+ return <div>{date?.toLocaleDateString()}</div>
373
+ }
374
+
375
+ // When navigating:
376
+ ;<Link search={(prev) => ({ ...prev, startDate: new Date().toISOString() })}>
377
+ Set Start Date
378
+ </Link>
379
+ ```