raw URLSearchParams → typed, validated, defaulted
A URL search string like ?page=2&sort=desc is raw text —
URLSearchParams hands you {{page:"2", sort:"desc"}}:
everything is a string, nothing is checked, nothing
has a default. TanStack Router’s validateSearch replaces that
with a schema (Zod / Valibot / ArkType). The router
runs a pipeline — parse → coerce → validate → default
— and the component reads route.useSearch() to get a real
{'{ page: number, sort: "asc"|"desc", filter?: string }'}. Invalid
URLs are rejected (or recovered with fallbacks), never silently passed through.
| stage | what happens | example |
|---|---|---|
parse |
URL search string → object of raw strings | ?page=2 → {'{ page:"2" }'} |
coerce |
declared type converts the string | z.number(): "2" → 2 |
validate |
constraints checked; reject on failure | .min(1), z.enum([...]) |
default |
missing key → schema default | no page → 1 |
type |
validator return type flows to consumers | route.useSearch() is fully typed |
1 · the schema validator you write (edit me)
searchSchema is the single source of truth (this stands in for
z.object({'{...}'})). validateSearch() is the engine:
it walks each field, coerces numbers, checks enum/min constraints, and fills
defaults for missing keys — returning {'{ data, errors }'},
exactly like a Zod parse result.
2 · Babel compiles JSX → element tree
<SearchValidationDemo/> becomes
React.createElement(SearchValidationDemo). The schema + validator
run as plain JS on every keystroke; React only re-renders the verdict when the
parsed input changes.
// (hit "compile & render" to see Babel's output)
3 · live search-param validator (proven)
Edit page / sort / filter above and watch the
verdict flip. Try page=invalid (number error), sort=unknown
(enum error), or clear a field (default applied). The gold-check
drives all four cases automatically.
schema params: — · gold: valid(2) → number error → enum error → defaults(1)
intent → schema pattern
| intent | pattern | why |
|---|---|---|
| required number param | z.number().int().min(1) |
rejects non-numbers and out-of-range; no silent "2" |
| default when missing | .default(1) |
applied only when the key is absent (parse-level) |
| recover when invalid | fallback(z.number(), 1) / .catch(1) |
keeps the route alive instead of throwing to errorComponent |
| constrain to a set | z.enum(['asc','desc']) |
type narrows to the union; unknown value → error |
| optional param | .optional() |
omitted key → undefined (not an error) |
| read in a component | route.useSearch() |
typed by the schema’s output; never raw strings |
| handle invalid URL | errorComponent: ({{ error }}) => ... |
shown when validation throws; reset via router.navigate |