Decision. Do not introduce Redux. Generate the API client from Forge’s OpenAPI document with Orval, let TanStack Query own server state, let TanStack Router own filter/sort/page state in typed URL search params, and leave one small Zustand store for theme, sidebar and column visibility. Redux’s official FAQ still says “don’t use Redux until you have problems with vanilla React” [1] — an admin console over a REST API does not have those problems.
The question dissolves when you name the four kinds of state
“Which state manager” is the wrong question, and every angle reached that conclusion independently. Enumerate what itenium-ui actually holds and each bucket has an owner that is not a global store:
- Server data — nearly all of it. TanStack Query’s own docs say that once async code moves into the cache, the truly global client state left over “is usually very tiny”, reducing their worked example to
themeModeandsidebarStatus[2]. - Filters, sort, page, selected row — the single largest fake use-case for a store. TanStack Router validates search params against a schema and type-checks every
navigate()against it [3]. Put them in a store instead and you get state that is not shareable, not bookmarkable, lost on refresh, with a broken back button. - Session and capabilities — server state, not client state.
react-oidc-contextalready exposes the token as a Context; copying it into a store creates a second copy that goes stale. - UI residue — theme, sidebar, column visibility. One persisted Zustand store, ~12 lines, no Provider.
Redux is not dying — 23.6M weekly downloads and quarterly releases [4] — but there is no bucket left for it to own. The three moats people still cite have all fallen: Zustand’s devtools middleware gives time-travel through the same Redux DevTools extension [5], and Redux’s own testing guide calls its store “an implementation detail of the app” that in many cases needs no explicit tests [6] — so a TDD mandate is not an argument for Redux either.
The layers compose — that is the actual finding
The pieces are not four independent picks that happen to co-exist. The router’s loader calls queryClient.ensureQueryData() and the component reads the same cache with useSuspenseQuery(); the router is explicitly designed as “a perfect coordinator for external data fetching and caching libraries” [7]. The typed search params are the query key. And Orval generates the hooks and the key factories from the OpenAPI document, so the layer you would otherwise hand-write mostly ceases to exist [8]. One custom mutator makes ProblemDetails the error type of every generated hook [9], which is the cleanest thing Forge’s RFC 7807 discipline buys the frontend.
Two findings point back at the backend
The frontend decision is not frontend-only. First, the README’s own worry — that baking capabilities into the JWT makes tokens too large — resolves by moving them out: keep the JWT to sub plus coarse roles and serve fine-grained capabilities from GET /me, cached in the query layer, which also buys revocation without re-login. Second, stay on Swashbuckle for now: .NET 10’s built-in OpenAPI generator emits anyOf without a discriminator object for polymorphic types [10], and without a discriminator no generator can emit a TypeScript discriminated union.
What it costs, honestly
Four new dependencies plus a codegen step, three of them from one vendor — that is real concentration risk in the TanStack family, and TanStack Router at 39.6 kB gzip is not free. Orval’s generated one-hook-per-endpoint style is also on the wrong side of the ecosystem’s drift toward composable queryOptions objects, which is the single reason to prefer Hey API instead. The security angle is the least comfortable: in-memory tokens are a damage limiter, not a fix, and the IETF’s browser-apps BCP reserves its top-ranked architecture — a backend-for-frontend — for exactly the class of app Forge is built for [11]. The open question this run does not settle: whether to put a .NET BFF in front of itenium-ui now, while the frontend is still greenfield and the migration is nearly free, or to ship the pure SPA and pay for it later.
Two proposed angles — a dedicated pass on testing each candidate, and one on lock-in and migration cost — were offered and left unticked, so they were not researched separately; MSW/Jest coverage appears inside the codegen and client-state angles instead.