Atlas recon

The router is a state manager: TanStack Router for itenium-ui

For itenium-ui — a greenfield React 19 + Vite SPA with no SSR — pick TanStack Router: it is the only option that makes filters/sort/pagination typed URL state instead of Redux slices, and it composes with TanStack Query rather than competing.

16 sources ~3 min read #2 react · routing · tanstack-router · react-router · state-management · spa · vite · nx

Decision — use TanStack Router ⭐ 15k (Jul 2026) [8], file-based routing, autoCodeSplitting, with TanStack Query underneath. It is the only one of the three that treats search params as validated, typed, first-class state [1][2] — which deletes the single largest fake use-case for a global store in an admin app. React Router v8 ⭐ 56k [9] is the safe, boring default [12] but is built around a server you do not have; Wouter ⭐ 7.9k [10] is a 2.6 kB <Link>/<Route> primitive that owns no state at all — for a role-guarded admin app it is under-powered.

The constraint nobody prices in

An admin app’s “global state” is mostly filters, sort, page, page-size, selected row, view mode, date range. Put those in Redux/Zustand and you get: not shareable, not bookmarkable, lost on refresh, back-button broken, and a second source of truth to sync with the URL. TanStack Router’s position — routes declare a Zod/Valibot schema for their search params, and every <Link>/navigate() writing them is type-checked against it, with JSON-serializable nested objects and arrays, not just flat strings [1][2]. Components subscribe with useSearch and re-render only on the params they read.

Net effect on the state stack: server state → TanStack Query; URL/filter state → the router; genuinely global client state → a small Zustand store (theme, sidebar, command palette). There is nothing left for Redux [11]. React Router’s declarative mode hands you raw useSearchParams (untyped strings) — the filter schema then has to live somewhere, and it drifts into a store.

Loaders vs the query cache: compose, don’t compete

They compose. The route loader calls queryClient.ensureQueryData(); the component reads the same cache with useSuspenseQuery(). Router coordinates when to fetch, Query owns caching/invalidation/refetch — the router is explicitly designed as “a perfect coordinator for external data fetching and caching libraries” [3]. Typed search params are the query key. That is the whole point: the URL is the query key is the state.

Scorecard

  TanStack Router ⭐ 15k React Router v8 ⭐ 56k Wouter ⭐ 7.9k
Typed params and search params ✓ end-to-end, schema-validated [2] params typed in framework mode via typegen; search params raw [6]
Search params as a store ✓ core feature [1] ✗ roll your own
Loaders without a server ✓ client-first [3] data/framework mode only; declarative mode has no loader [6]
Auth guard beforeLoad + router context, throw redirect(), parent guards whole subtree [4] loader-based redirect manual
Code-splitting autoCodeSplitting splits critical vs non-critical automatically [5] ✓ framework mode manual
SSR-neutral ✓ but v8 is ESM-only, Vite 7+, framework mode assumes a build server [7]
gzip 39.6 kB [14] 59.0 kB [15] 2.6 kB [16]

Caveats. (1) TanStack’s read story is excellent; writing search params over time (composable updates, defaults, lifecycle) is still low-level and teams end up building a thin helper layer [13] ⭐ 15k. (2) React Router has 3.8× the stars and shipped v8 in June 2026 with a yearly-major cadence and open governance [7] — it is not a risky choice, it is a mismatched one for a pure SPA with a .NET API.

Nx fit

Use file-based routing per app (apps/itenium-ui/src/routes/**), not code-based: autoCodeSplitting requires the Vite plugin and is the reason to keep the file convention [5]. The generated routeTree.gen.ts is per-app, so the two apps stay independent; shared route fragments don’t belong in libs/ — shared components and search-param schemas do. Auth guards live in one _authenticated layout route whose beforeLoad reads Keycloak roles/capabilities off the JWT in router context and throw redirect()s [4]; Forge capability checks are just context.auth.can(...) in that one place.

What the router owns (and Redux therefore does not)

page, pageSize, sort, sortDir, q, filters{}, dateRange, view, selectedId, tab, redirect (post-login). Each declared once in a route’s validateSearch schema, each type-checked at every call-site, each free in a shared link.

Citations · 16 sources

Click the Citations tab to load…