Router edges: pinned TanStack versions, pending and retrying error pages, zod search params #407

Closed
opened 2026-10-15 12:31:46 +00:00 by root · 7 comments
root commented 2026-10-15 12:31:46 +00:00 (Migrated from gitlab.core-project.space)

Problem Statement

The front's TanStack Router setup follows the docs at its core (loaders prime Query, components read with useSuspenseQuery, intent preloading with defaultPreloadStaleTime: 0, automatic code splitting), but its edges lag behind:

  • A route whose loader is slow shows nothing new while the Player waits: no route and no router default defines a pending UI.
  • A route whose loader fails shows a one-line error with no way out but reloading the browser.
  • Two search parameters (the account page's tab, My games' result filter) are checked by hand-written validators instead of a schema, and the harness re-declares one of them.
  • Five TanStack packages are on latest, @tanstack/router-plugin is declared twice, and the route generator's CLI config does not match the Vite plugin's, so a fresh install or generate-routes can change behaviour or rewrite the committed route tree. The live TanStack docs already describe APIs our lockfile does not have (queryClient.query(), Query 5.102).

Source: the front audit, docs/research/front-audit.md, sections 1 (points 2-4) and 2.1.

Solution

  • Every TanStack package has a fixed version and one entry, and the route generator writes the same route tree whether it runs from the CLI or from Vite.
  • A Player waiting on a slow route sees a pending state after a short delay, never a flash.
  • A Player whose page failed to load reads why, in their language, and can try again in place.
  • The account page's tab and My games' filter are validated by zod schemas the router reads directly; an unknown value still falls back to the default.

User Stories

  1. As a maintainer, I want every TanStack package pinned to a known version, so that an install never changes the router or Query under me.
  2. As a maintainer, I want @tanstack/router-plugin declared once, so that the version that runs is the one I read.
  3. As a maintainer, I want generate-routes and the Vite build to write the same route tree, so that neither leaves a diff behind the other.
  4. As a Player whose next page takes longer than a moment to load, I want a pending state, so that I know the click was taken.
  5. As a Player whose next page loads quickly, I don't want a pending state to flash, so that fast navigation stays calm.
  6. As a Player whose page failed to load, I want to read that something went wrong, in my language, so that I am not left with a blank screen.
  7. As a Player whose page failed to load, I want a "Try again" action that loads the page again in place, so that I don't have to reload the browser.
  8. As a Player using a screen reader, I want the error announced and the retry reachable by name, so that I can recover without seeing the page.
  9. As a Player, I want the account page's tab and My games' filter kept in the URL exactly as today, so that Back, a reload or a shared link still work.
  10. As a Player following a link with an unknown tab or filter, I want the default shown rather than an error, so that a stale link still opens the page.
  11. As a French-speaking Player, I want the pending and error copy in French, so that the site reads in my language.

Seams

  • S1 — the app's router, mounted by the router harness (front view test). Covers 4-8, 11: a loader held open shows the pending state only after the delay; a loader that fails shows the error, and "Try again" shows the page once the next read succeeds.
  • S2 — AccountPageView with the auth harness, and MyGamesView with the games harness (front view tests). Covers 9-10: the existing search-parameter tests stay green unchanged, plus an unknown-value case each.
  • S3 — the route generator and the gate (no new test). Covers 1-3: generate-routes on a clean tree leaves routeTree.gen.ts unchanged, and the gate is green on the pinned versions.
  • S4 — apps/e2e (e2e). Covers 6-7 in a real browser: the first read behind a page is failed once with Playwright's request interception, the error is shown, and "Try again" shows the page.

Implementation Decisions

  • Versions. Exact versions for the TanStack packages now on latest, at least the versions in today's lockfile. Bumping Query past 5.102 is not part of this spec; when it happens, ensureQueryData moves to queryClient.query() (ADR 0019 stays).
  • Generator config. tsr.config.json carries the same generator options as the Vite plugin (target, quote style, semicolons), so the CLI's output matches.
  • Pending UI. A router-wide defaultPendingComponent, keeping the router's default pendingMs/pendingMinMs unless a test shows a reason to tune them. It lives in app/, beside the existing router-wide error and not-found components.
  • Error UI. The existing router-wide error component gains the documented recovery: reset Query's error boundary on mount (useQueryErrorResetBoundary) and call router.invalidate() from "Try again". The copy is the existing site error plus a new "Try again" message.
  • Search params. validateSearch takes a zod 4 schema directly (no adapter), with .catch() so an unknown value falls back. Under today's lint rule a route imports only a vertical's view or services, so the schemas are exported from the views that own the choices; they move into the route files once the rules spec allows it. tryChoiceOf goes if nothing else uses it.
  • Copy (ADR 0025). Every new string goes into the Paraglide catalogue in English and French.
  • ADRs: 0011, 0019, 0025 apply. No new ADR.

Testing Decisions

  • Prior art: router.test.tsx (router-level behaviour through mountRouter), account-page.test.tsx and my-games.test.tsx (search parameters), app-shell.test.tsx (French copy).
  • Edge and unhappy paths: a loader that resolves before the pending delay (no pending state); a retry that fails again (error stays, retry still offered); an unknown section or result; a result given twice.
  • e2e: the suite has no request interception yet; this is its first use, and it adds no hook to the front or the API (e2e rule 3).

Out of Scope

  • Route guards (their own spec).
  • Moving search schemas into route files (after the rules spec).
  • Bumping TanStack Query past 5.102 and the queryClient.query() migration.
  • Per-route pending or error components.
  • Route masks and useBlocker (no use case today; useBlocker is experimental).

Further Notes

## Problem Statement The front's TanStack Router setup follows the docs at its core (loaders prime Query, components read with `useSuspenseQuery`, intent preloading with `defaultPreloadStaleTime: 0`, automatic code splitting), but its edges lag behind: - A route whose loader is slow shows nothing new while the Player waits: no route and no router default defines a pending UI. - A route whose loader fails shows a one-line error with no way out but reloading the browser. - Two search parameters (the account page's tab, My games' result filter) are checked by hand-written validators instead of a schema, and the harness re-declares one of them. - Five TanStack packages are on `latest`, `@tanstack/router-plugin` is declared twice, and the route generator's CLI config does not match the Vite plugin's, so a fresh install or `generate-routes` can change behaviour or rewrite the committed route tree. The live TanStack docs already describe APIs our lockfile does not have (`queryClient.query()`, Query 5.102). Source: the front audit, `docs/research/front-audit.md`, sections 1 (points 2-4) and 2.1. ## Solution - Every TanStack package has a fixed version and one entry, and the route generator writes the same route tree whether it runs from the CLI or from Vite. - A Player waiting on a slow route sees a pending state after a short delay, never a flash. - A Player whose page failed to load reads why, in their language, and can try again in place. - The account page's tab and My games' filter are validated by zod schemas the router reads directly; an unknown value still falls back to the default. ## User Stories 1. As a maintainer, I want every TanStack package pinned to a known version, so that an install never changes the router or Query under me. 2. As a maintainer, I want `@tanstack/router-plugin` declared once, so that the version that runs is the one I read. 3. As a maintainer, I want `generate-routes` and the Vite build to write the same route tree, so that neither leaves a diff behind the other. 4. As a Player whose next page takes longer than a moment to load, I want a pending state, so that I know the click was taken. 5. As a Player whose next page loads quickly, I don't want a pending state to flash, so that fast navigation stays calm. 6. As a Player whose page failed to load, I want to read that something went wrong, in my language, so that I am not left with a blank screen. 7. As a Player whose page failed to load, I want a "Try again" action that loads the page again in place, so that I don't have to reload the browser. 8. As a Player using a screen reader, I want the error announced and the retry reachable by name, so that I can recover without seeing the page. 9. As a Player, I want the account page's tab and My games' filter kept in the URL exactly as today, so that Back, a reload or a shared link still work. 10. As a Player following a link with an unknown tab or filter, I want the default shown rather than an error, so that a stale link still opens the page. 11. As a French-speaking Player, I want the pending and error copy in French, so that the site reads in my language. ## Seams - **S1 — the app's router, mounted by the router harness** (front view test). Covers 4-8, 11: a loader held open shows the pending state only after the delay; a loader that fails shows the error, and "Try again" shows the page once the next read succeeds. - **S2 — `AccountPageView` with the auth harness, and `MyGamesView` with the games harness** (front view tests). Covers 9-10: the existing search-parameter tests stay green unchanged, plus an unknown-value case each. - **S3 — the route generator and the gate** (no new test). Covers 1-3: `generate-routes` on a clean tree leaves `routeTree.gen.ts` unchanged, and the gate is green on the pinned versions. - **S4 — `apps/e2e`** (e2e). Covers 6-7 in a real browser: the first read behind a page is failed once with Playwright's request interception, the error is shown, and "Try again" shows the page. ## Implementation Decisions - **Versions.** Exact versions for the TanStack packages now on `latest`, at least the versions in today's lockfile. Bumping Query past 5.102 is not part of this spec; when it happens, `ensureQueryData` moves to `queryClient.query()` (ADR 0019 stays). - **Generator config.** `tsr.config.json` carries the same generator options as the Vite plugin (target, quote style, semicolons), so the CLI's output matches. - **Pending UI.** A router-wide `defaultPendingComponent`, keeping the router's default `pendingMs`/`pendingMinMs` unless a test shows a reason to tune them. It lives in `app/`, beside the existing router-wide error and not-found components. - **Error UI.** The existing router-wide error component gains the documented recovery: reset Query's error boundary on mount (`useQueryErrorResetBoundary`) and call `router.invalidate()` from "Try again". The copy is the existing site error plus a new "Try again" message. - **Search params.** `validateSearch` takes a zod 4 schema directly (no adapter), with `.catch()` so an unknown value falls back. Under today's lint rule a route imports only a vertical's view or services, so the schemas are exported from the views that own the choices; they move into the route files once the rules spec allows it. `tryChoiceOf` goes if nothing else uses it. - **Copy (ADR 0025).** Every new string goes into the Paraglide catalogue in English and French. - **ADRs:** 0011, 0019, 0025 apply. No new ADR. ## Testing Decisions - Prior art: `router.test.tsx` (router-level behaviour through `mountRouter`), `account-page.test.tsx` and `my-games.test.tsx` (search parameters), `app-shell.test.tsx` (French copy). - Edge and unhappy paths: a loader that resolves before the pending delay (no pending state); a retry that fails again (error stays, retry still offered); an unknown `section` or `result`; a `result` given twice. - e2e: the suite has no request interception yet; this is its first use, and it adds no hook to the front or the API (e2e rule 3). ## Out of Scope - Route guards (their own spec). - Moving search schemas into route files (after the rules spec). - Bumping TanStack Query past 5.102 and the `queryClient.query()` migration. - Per-route pending or error components. - Route masks and `useBlocker` (no use case today; `useBlocker` is experimental). ## Further Notes - Audit: `docs/research/front-audit.md` on branch `claude/nestjs-best-practices-audit-716d79`. - Independent of the other three specs from the audit; can start first. - Docs: <https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#showing-a-pending-component>, <https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading#error-handling-with-tanstack-query>, <https://tanstack.com/router/latest/docs/framework/react/guide/search-params#zod>.
root commented 2026-10-15 12:33:14 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #411

mentioned in issue #411
root commented 2026-10-15 12:33:15 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #411

marked this issue as related to #411
root commented 2026-10-15 12:33:31 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #412

marked this issue as related to #412
root commented 2026-10-15 12:33:31 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #412

mentioned in issue #412
root commented 2026-10-15 12:33:33 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #413

mentioned in issue #413
root commented 2026-10-15 12:33:33 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #413

marked this issue as related to #413
root commented 2026-10-15 12:57:59 +00:00 (Migrated from gitlab.core-project.space)

mentioned in merge request !533

mentioned in merge request !533
root (Migrated from gitlab.core-project.space) closed this issue 2026-10-16 06:34:35 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
amaurycoudr/chess-project#407
No description provided.