Atlas survey

Type-Safe Routing & Typed Search-Param State in React (2026)

TanStack Router is the only router with typed, schema-validated search-param state end to end — the decisive edge for a filter-heavy admin console, at the cost of advanced-type complexity.

15 sources ~6 min read #8 react · typescript · routing · tanstack-router · react-router · search-params · spa

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 — no as casts, no manual Number(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]. Raw URLSearchParams is 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.

WouteruseSearch() 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/tsc load 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.

Citations · 15 sources

Click the Citations tab to load…