Validate search params with defaults at the route
Give every route that reads search params a validateSearch that parses them, rejects or replaces invalid values, and supplies defaults.
Read them only through the route's typed APIs, such as Route.useSearch().
Implementation
- Use a schema library, such as Zod or Valibot, with a fallback or
catchfor each field, so an invalid value falls back to its default instead of failing the route. Recent router versions accept Standard Schema validators directly invalidateSearch. - Write a manual validator only for a few simple fields, and check each value's type explicitly.
- Read search params with
Route.useSearch()orgetRouteApi(...).useSearch(), never fromwindow.location. - Update them with
navigate({ search: (prev) => ({ ...prev, ...changes }) })orLink'ssearch, resetting dependent values such aspagewhen filters change. - Search params are inherited by child routes, so validate shared ones in the parent.
- To change how search params appear in the URL, set the router's
parseSearchandstringifySearchoptions together, built withparseSearchWithandstringifySearchWithso parsing and writing stay inverses. Validation still runs on the parsed values.
Rationale
Users edit URLs, share old links, and follow links from other sites, so a search param may be missing, malformed, or out of range. Validating at the route turns every URL into a known, typed shape once, instead of each component guessing, and gives every param a sensible default.
Examples
Application: Reading raw search params
Incorrect (counterexample):
function ProductsPage() {
const params = new URLSearchParams(window.location.search);
const page = parseInt(params.get('page') ?? '1');
const sort = params.get('sort') as 'asc' | 'desc';
// ...
}
?page=abc produces NaN, and sort can be any string despite its type.
Correct:
const productSearchSchema = z.object({
page: z.number().int().min(1).catch(1),
sort: z.enum(['asc', 'desc']).catch('asc'),
category: z.string().optional().catch(undefined),
});
export const Route = createFileRoute('/products')({
validateSearch: productSearchSchema,
component: ProductsPage,
});
function ProductsPage() {
const { page, sort, category } = Route.useSearch();
// ...
}
Application: A manual validator
Incorrect (counterexample):
validateSearch: (search: Record<string, unknown>) => ({
minPrice: Number(search.minPrice) || undefined,
}),
A minimum price of 0 becomes undefined, because 0 is falsy.
Correct:
validateSearch: (search: Record<string, unknown>) => ({
minPrice: typeof search.minPrice === 'number' && search.minPrice >= 0 ? search.minPrice : undefined,
}),
Validation
Open the route with missing, malformed, and out-of-range search params, such as ?page=abc&sort=sideways, and check that it renders with defaults.
Search components for reads from window.location.search or URLSearchParams.
A route that reads no search params does not need validateSearch.