Repository navigation
File Based Router Migration 20261022
Replaced the hand-built TanStack Router route registry with file-based routing. Routes are now files under frontend/src/routes/ — the folder structure is the URL structure — and the route tree is generated into src/routeTree.gen.ts by the TanStack Router Vite plugin. The old registry (routePaths.tsx with its RouteDescription[] arrays), the hand-built tree and router singleton (routeTree.tsx), the AppRouter.tsx wrapper, the applyGuards HOC chain, and all five guard HOCs were deleted. Access control moved to pathless layout routes and beforeLoad hooks.
Delivered on branch chore/route-rework (PR #1447), which also merged the reporting-unit search work (#1238) from main and adopted its route-owned search state.
- PR #1447 — routing migration cutover
- #1238 — reporting-unit search source toggle;
/searchfilter state became route-owned URL state
- Make the route tree self-evident: reading
src/routes/tells you every URL, gate, and nesting relationship without opening a registry file - Remove the duplicated bookkeeping between the route registry, the menu registry, and the router wiring
- Keep the exact access-control behavior users saw before: no-flash redirects, deep-link preservation through login, fail-closed feature flags, one-shell 404s
- Keep navigation fully typed end to end
Old system (deleted):
| Old piece | What it did |
|---|---|
src/routes/routePaths.tsx |
ROUTES / SYSTEM_ROUTES arrays of RouteDescription objects — path, component, guards, menu metadata |
src/routes/routeTree.tsx |
Hand-built route tree, applyGuards() HOC composition, router singleton, module augmentation |
src/routes/AppRouter.tsx |
Thin <RouterProvider> wrapper |
src/routes/guards/withProtected.tsx |
HOC: auth + status + optional route-level roles |
src/routes/guards/withPublicOnly.tsx |
HOC: bounce authenticated users from public pages |
src/routes/guards/withOfflineSupport.tsx |
HOC: connectivity-based route blocking |
src/routes/guards/withPersistentRedirect.tsx |
HOC: post-login redirect resolution on /dashboard
|
src/routes/guards/withFeatureFlag.tsx |
HOC: feature-flag route gating |
src/routes/inTreePaths.ts |
InTreePath union + navigateInTree cast shim |
src/hooks/useSyncFiltersToSearchParams |
Bidirectional filter ↔ URL sync hook (merged from main, then superseded) |
New system:
| New piece | What it does |
|---|---|
src/routes/ — 29 route files |
One file per route; folder placement decides auth/role gating |
src/routeTree.gen.ts |
Generated tree, committed (see below) |
src/router.tsx |
createAppRouter(context, { history? }) factory + browser-history singleton + the single Register augmentation |
src/routerContext.ts |
AppRouterContext = { queryClient } |
src/routes/_authenticated.tsx |
Pathless layout: auth + status gate, one <Layout> shell |
src/routes/_authenticated/_admin.tsx, _client-access.tsx
|
Pathless role-tier layouts (ADMIN; any-of VIEWER/SUBMITTER) |
src/routes/_authenticated/$.tsx |
Catch-all 404 inside the shell |
src/routes/dashboard.tsx |
DashboardRedirect — post-login redirect resolver |
src/routing/auth.ts |
Pure computeAuthRedirect / hasAnyRole decision logic |
src/routing/staticData.ts |
StaticDataRouteOption augmentation + NavTarget union |
src/routing/nav/useNavEntries.ts + routeMetadata.ts
|
Side nav projected from route staticData with ancestor-chain gate inheritance |
src/routing/searchParams.ts |
Zod validateWasteSearch for /search URL state + router-level searchParamCodec
|
Auth as a layout, not beforeLoad. Authentication state lives in React context (useAuth). A beforeLoad hook runs outside React and cannot read that context — enforcing auth there would require a second, non-React snapshot of the auth state, and the two sources could disagree mid-refresh. The _authenticated layout reads the same context the rest of the app uses, so there is exactly one source of truth.
Feature flags in beforeLoad, not a layout. Flags are synchronous environment values, readable anywhere, that never live in React context. Each check lives where its data lives. The ordering consequence is deliberate: beforeLoad runs before the React auth gate, so an unauthenticated user opening a disabled route gets the auth-aware 404 (sent to / to sign in) rather than a login prompt for a page that does not exist — the route fails closed. Likewise an authenticated non-admin on a disabled /configuration gets a 404 rather than /unauthorized, because the flag check precedes the role tier. Disabled means absent, for everyone.
A single Register on the new router. The generated route tree already augments the router's types through its FileRoutesByPath interface. A second Register module augmentation cannot coexist with it: the two compete and the navigation to union collapses to never, breaking every typed navigation at compile time. The augmentation lives once, next to the router that owns it.
A catch-all under _authenticated. Unknown URLs are claimed by _authenticated/$ so authenticated 404s render in exactly one application shell (header + side nav + "Content Not Found"). The one exception: an unauthenticated visitor on an unknown URL is redirected to / without persisting a redirect target — there is no real destination to restore after login, and the legacy behavior never persisted one either.
The generated tree is committed. The build runs tsc -b tsconfig.app.json && vite build — type-check executes first, so a clean checkout without src/routeTree.gen.ts fails before the Vite plugin ever gets a chance to generate it. Committing the file keeps clean checkouts and CI working; the plugin rewrites it whenever routes change.
useLayoutEffect, not useEffect, for gate redirects. Layout effects commit before the browser paints, so a denied user never sees a frame of gated content. This preserves the previous guard generation's no-flash timing exactly.
Effect dependencies must be primitives. The auth layout's redirect effect originally depended on the redirect decision object, which is a fresh { href } every render. The effect re-fired on every transition and each new navigate() cancelled the pending one — an infinite navigate/cancel loop that never committed the hop. The routing access-matrix tests caught the loop; the fix was depending on the primitive redirectHref string instead. This rule now applies to every gate effect.
routerContext.ts as a separate module. Route files and loaders need the AppRouterContext type. If it lived in router.tsx, importing the type from a route file would pull the generated tree (and the whole router) into every route module — a runtime import cycle. The isolated type module keeps route imports clean.
staticData typed via @tanstack/router-core. The augmentation targets @tanstack/router-core's StaticDataRouteOption because @tanstack/react-router only re-exports that interface — augmenting the re-export has no effect on the staticData property route files use.
Nav from route metadata. The side nav is projected from staticData.nav (label, icon, order, literal NavTarget), with gate metadata (roles, featureFlag, offline flags) inherited by walking the ancestor chain — staticData is per-route and never merged by the router. The Configuration menu entry is data on _admin/configuration.tsx, not code in a menu registry: one place to declare a route, one place to keep its menu presence in sync.
Route-owned search state. /search validates its URL through validateSearch: validateWasteSearch; the Zod schema in src/routing/searchParams.ts is the only place filter parameter types are decided. The router-level searchParamCodec writes plain, human-readable URLs (strings unquoted, arrays comma-joined, booleans true/false) while still parsing legacy JSON-encoded URLs. The hook-based bidirectional sync (useSyncFiltersToSearchParams, merged from main shortly before the cutover) was deleted in the same change rather than maintained twice — the URL-as-source-of-truth model makes a sync hook redundant.
Vite plugin before react(). tanstackRouter({ target: 'react', autoCodeSplitting: true, routeFileIgnorePattern: ... }) must run before react() so the generated tree exists when route modules are transformed. routeFileIgnorePattern keeps colocated unit/e2e test files out of the route tree. src/routes/ holds route files only; shared helpers live in src/routing/ so the generator never mistakes a helper for a route.
-
autoCodeSplitting: true— each route component lands in its own chunk without manualReact.lazy -
defaultPreload: 'intent'withdefaultPreloadStaleTime: 30_000— hover/focus preloads reuse data for 30 seconds instead of refetching
- Navigation is typed via the single
Register:navigate({ to, params })andLink toaccept only real route paths; numeric params are passed asString(...) -
hrefremains the documented escape hatch for the two runtime-composed string targets:useListTableRowActions(paths templated from table config) andPageTitlebreadcrumbs - Route loaders receive
{ context, params }and pre-fetch throughcontext.queryClient.ensureQueryData; flag checks moved out of loaders intobeforeLoad -
/searchfilter state is read from validated route search;legacy=falseis preserved on the wire because absent means the legacy search source
- Deleted: route registry, hand-built tree,
AppRouter.tsx, all five guard HOCs,applyGuards,navigateInTree/InTreePathshim,useSyncFiltersToSearchParams - Offline metadata (
offlineOnly/offlineReady) survives asstaticDataconsumed by the nav projection only — connectivity no longer blocks any route
- Fixed the infinite navigate/cancel loop in the auth layout (object-valued effect dependency) found by the routing access-matrix tests
- 29 route files, 19 URLs, one generated tree — the file system is the source of truth for routing
-
Access control is structural: auth by living under
_authenticated, roles by living under a tier folder, flags by declaring abeforeLoadgate -
One shell:
_authenticatedrenders a single<Layout>; pages, errors, and 404s all render in exactly one shell - Maintenance surface reduced: no registry, no menu list, no guard composition order to keep in sync; adding a route is creating a file
- Wiki pages rewritten:
architecture/frontend/Routing.md(full rewrite),codes/frontend/routes/RouteGuards.mdandRoutesIndex.md(full rewrites); stale references corrected inFeature-Flags.md,architecture/frontend/Authentication.md,Roles.md,Frontend Structure.md,Frontend Architecture Overview.md, and the codes pages touched by the sweep
- Keep the access-matrix test in step with every new route — it is the regression net for gate interactions
- Watch for accidental
Registeraugmentations in new modules; there must be exactly one - When a new role tier is needed, add the
_tier.tsxlayout + folder rather than per-route role declarations
This wiki documents NR Waste Plus for users and developers. Verify security and deployment guidance against the current application configuration before applying it elsewhere.
- Report an issue or ask a question in the GitHub repository.
- Follow the Contributing Guidelines to propose an update.
- Read the Code of Conduct.
Collection indexes support human browsing and machine retrieval. Keep both current when adding or moving wiki pages.
NR Waste Plus Wiki | GitHub Repository