Decision: For an itenium-style admin console with many filterable tables and deep-linkable state, TanStack Router ⭐ 15k (Jul 2026) is the only router that makes typed, schema-validated search params a first-class part of the type system [6]. React Router v7 ⭐ 56k gets you typed path params and loader data via codegen — but only in framework mode, and its search params stay an untyped
URLSearchParams[3][5]. Wouter ⭐ 7.9k has essentially no typing or validation story [9]. Pay the TanStack complexity tax only if URL-as-state is central — which, for filter-heavy tables, it is.
Why typed search-param state matters here
An admin console’s URL is application state: filters, sort, pagination, view mode, selected row. TanStack’s thesis is that these are serializable, shareable, global state that deserves the same rigor as any store — without a route-level schema, components invent conflicting shapes, defaults drift, and deep links break [1]. For an app with dozens of filterable tables, that drift is the actual maintenance cost, and it’s exactly what typed search params eliminate: filters, sorting, pagination and view modes “belong in the URL, and they need validation and typed access” [10].
Two capabilities carry the weight:
- Typed reads.
useSearch()returns a fully-typed, parsed, defaulted object — noascasts, no manualNumber(params.get('page'))[2]. - Rich serialization. TanStack serializes search with
JSON.stringify/parse, so numbers, booleans, arrays and nested objects round-trip through the URL natively —{ page: 1, tags: ['sale'] }↔?page=1&tags=["sale"][14]. RawURLSearchParamsis strings-only.
The type-safety story per router
| Axis | TanStack Router ⭐ 15k | React Router v7 ⭐ 56k | Wouter ⭐ 7.9k |
|---|---|---|---|
| Typed path params | ✓ inferred from route tree [6] | ✓ via typegen → +types (framework mode only) [3] |
✗ [9] |
| Typed search/query params | ✓ schema-validated, first-class [2] | ✗ untyped URLSearchParams, manual parse/cast [5] |
✗ useSearch() string only, no validation [9] |
Typed <Link> / href |
✓ to + search type-checked [1] |
✓ typed href in framework mode [3] |
✗ |
| Typed loader data | ✓ zero-config inference [6] | ✓ framework mode; ⚠ SPA mode needs as casts [11] |
n/a |
| How types are produced | Live TS inference, no build step | Codegen (react-router typegen) + rootDirs [4] |
— |
| Best fit | Client SPAs, dashboards, admin, search-driven UIs [10] | Full-stack SSR / framework apps [15] | Tiny apps, minimal URL state |
The critical asymmetry for a client-rendered SPA: React Router’s strongest type safety lives in framework mode (SSR, route modules, routes.ts codegen). Run it as a pure SPA and you drop back to manual casting — useLoaderData() as Post, untyped search [5][11]. TanStack’s typing is the same whether you SSR or not.
Code shape
TanStack — schema is the source of truth; reads are typed:
export const Route = createFileRoute('/users')({
validateSearch: z.object({
page: z.number().default(1),
status: z.enum(['active', 'invited']).optional(),
tags: z.array(z.string()).default([]),
}),
})
function Users() {
const { page, status, tags } = Route.useSearch() // fully typed, parsed, defaulted
// typed navigation; TS errors if `status` isn't in the enum
return <Link to="/users" search={(p) => ({ ...p, page: p.page + 1 })}>Next</Link>
}
Per-field fallback() substitutes a safe value instead of throwing on bad input; parent routes can define shared search schemas that children extend [2].
React Router v7 — path params typed, search hand-rolled:
import type { Route } from './+types/users'
export function loader({ params }: Route.LoaderArgs) { /* params.id typed */ }
function Users() {
const [sp] = useSearchParams()
const page = Number(sp.get('page') ?? '1') // untyped string; you own the coercion
const status = sp.get('status') as 'active' | 'invited' | null // manual cast
}
To close the search-param gap you reach for a library like react-router-typesafe-routes ⭐ 167, which adds validated typing for path, search, state and hash [12] — extra dependency, extra ceremony.
Wouter — useSearch() returns the raw query string; parsing, typing and validation are entirely yours [9]. Fine for a handful of routes, wrong tool for a filter-heavy console.
The honest cost of full type safety
TanStack’s inference isn’t free:
- Advanced-type complexity. It leans on “very advanced and complex types and type inference” and deliberately deviates from routing norms to get there [7]. Type errors can be deep and cryptic; IDE/
tscload on a large route tree is real. - Config sprawls across files. The single-file route ideal breaks down once you add nested context, loaders and search validation — it becomes “infeasible to define routes in a single file,” pushing you to multi-file conventions [8].
- Learning curve. Not beginner-friendly; the payoff assumes a TypeScript-first team [7].
- Marginal bundle delta — ~40KB vs ~32KB for React Router — not a deciding factor [5].
React Router’s cost is different: a typegen build step wired through rootDirs, and the accepted reality that search params stay untyped unless you bolt on a library [4][12].
Bottom line for the admin console
If deep-linkable, filterable, sortable tables are the core of the app, typed search-param state is not a nice-to-have — it’s the feature that stops URL-state rot at scale, and only TanStack Router provides it natively and validated [1][6]. Accept the advanced-type complexity as the price. Choose React Router v7 instead if you expect to grow into SSR/framework mode or want the largest ecosystem and least migration friction [15], and plan to add a typed-routes library for the search-param gap. Wouter and micro-routers like TypeRoute ⭐ 52 [13] are out of scope for an app this URL-stateful.