From c833d6f3dd7a9a08189a3e6d9dcfc34bedb28fc6 Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Sat, 2 May 2026 20:00:20 +0200 Subject: [PATCH] fix(routing): redirect anonymous users from protected routes + devtools rename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1: hard-loading / while unauthenticated stayed stuck on "Loading..." forever. Cause: the _authed layout short-circuits to the spinner when status is anything other than 'authenticated', which means child routes never mount. The redirect-on-anonymous useEffect lived only in the home page (_authed/index.tsx), so it never fired — the layout's spinner was the last thing rendered. Fix: add a useEffect to the _authed layout component itself that navigates to /login on the 'anonymous' transition. The layout's gate is now: beforeLoad redirects on cold-known-anonymous, useEffect redirects on post-mount transitions to anonymous (e.g. boot probe resolves to anonymous after the route already rendered). Bug 2: console warning that @tanstack/router-devtools moved to @tanstack/react-router-devtools. Same package, renamed. Fix: pnpm remove @tanstack/router-devtools && pnpm add -D @tanstack/react-router-devtools; updated the lazy import in __root.tsx to point at the new package name. Plus: TRM_Design_System-handoff/ added to .prettierignore — those files are immutable source material from claude.ai/design and shouldn't be reformatted. --- .prettierignore | 4 ++++ package.json | 2 +- pnpm-lock.yaml | 31 +++---------------------------- src/routes/__root.tsx | 2 +- src/routes/_authed/route.tsx | 34 +++++++++++++++++++++++++--------- 5 files changed, 34 insertions(+), 39 deletions(-) diff --git a/.prettierignore b/.prettierignore index 6409ae9..67c61ec 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,3 +11,7 @@ pnpm-lock.yaml public src/routeTree.gen.ts +# Design handoff bundle — immutable source material from claude.ai/design. +# Do not auto-format; preserve as-shipped for reference. +TRM_Design_System-handoff + diff --git a/package.json b/package.json index 200ef3a..03d5528 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "@eslint/js": "^10.0.1", "@tailwindcss/vite": "^4.2.4", "@tanstack/react-query-devtools": "^5.100.8", + "@tanstack/react-router-devtools": "^1.166.13", "@tanstack/router-cli": "^1.166.40", - "@tanstack/router-devtools": "^1.166.13", "@tanstack/router-plugin": "^1.167.32", "@types/node": "^24.12.2", "@types/react": "^19.2.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91cc348..c85f8ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,12 +60,12 @@ importers: '@tanstack/react-query-devtools': specifier: ^5.100.8 version: 5.100.8(@tanstack/react-query@5.100.8(react@19.2.5))(react@19.2.5) + '@tanstack/react-router-devtools': + specifier: ^1.166.13 + version: 1.166.13(@tanstack/react-router@1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.169.1)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/router-cli': specifier: ^1.166.40 version: 1.166.40 - '@tanstack/router-devtools': - specifier: ^1.166.13 - version: 1.166.13(@tanstack/react-router@1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.169.1)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/router-plugin': specifier: ^1.167.32 version: 1.167.32(@tanstack/react-router@1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)) @@ -1275,18 +1275,6 @@ packages: csstype: optional: true - '@tanstack/router-devtools@1.166.13': - resolution: {integrity: sha512-Qs8gkyI7m+eAxG3VcIOHuTSsUfA5ZxZcOa99ZyIIIJFxW6hy1k+m2s1J0ZYN1SNlip8P2ofd/MHiqmR1IWipMg==} - engines: {node: '>=20.19'} - peerDependencies: - '@tanstack/react-router': ^1.168.15 - csstype: ^3.0.10 - react: '>=18.0.0 || >=19.0.0' - react-dom: '>=18.0.0 || >=19.0.0' - peerDependenciesMeta: - csstype: - optional: true - '@tanstack/router-generator@1.166.39': resolution: {integrity: sha512-j2OW/UvpjM/DT9tHVmuhWW1k6UOezTRrPqBPZFFmIth0fY7iTPqK+Erqpo8r5yGTRGCbMvOS4sL3H2MldnIZew==} engines: {node: '>=20.19'} @@ -3435,19 +3423,6 @@ snapshots: optionalDependencies: csstype: 3.2.3 - '@tanstack/router-devtools@1.166.13(@tanstack/react-router@1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.169.1)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@tanstack/react-router': 1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@tanstack/react-router-devtools': 1.166.13(@tanstack/react-router@1.169.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.169.1)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - clsx: 2.1.1 - goober: 2.1.18(csstype@3.2.3) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - optionalDependencies: - csstype: 3.2.3 - transitivePeerDependencies: - - '@tanstack/router-core' - '@tanstack/router-generator@1.166.39': dependencies: '@babel/types': 7.29.0 diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index b24ff48..371db74 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -8,7 +8,7 @@ import { queryClient } from '@/lib/query-client'; // `import.meta.env.DEV`. const TanStackRouterDevtools = import.meta.env.DEV ? lazy(() => - import('@tanstack/router-devtools').then((mod) => ({ + import('@tanstack/react-router-devtools').then((mod) => ({ default: mod.TanStackRouterDevtools, })), ) diff --git a/src/routes/_authed/route.tsx b/src/routes/_authed/route.tsx index f750a0e..3226add 100644 --- a/src/routes/_authed/route.tsx +++ b/src/routes/_authed/route.tsx @@ -1,18 +1,23 @@ -import { Outlet, createFileRoute, redirect } from '@tanstack/react-router'; +import { useEffect } from 'react'; +import { Outlet, createFileRoute, redirect, useNavigate } from '@tanstack/react-router'; import { useAuthStore } from '@/auth'; /** * Pathless layout for all protected routes. * - * `beforeLoad` is the canonical TanStack Router gate — runs before any - * loader/component on this branch. If the user is anonymous when the - * router resolves the route, redirect to `/login` carrying the intended - * destination as `?redirect=`. + * Two complementary gates: * - * If status is `'unknown'` or `'authenticating'` (boot probe still in - * flight), let the route render — the layout component below shows a - * loading placeholder until the probe resolves. Without that, the user - * gets an unwanted login flash on every hard reload. + * 1. `beforeLoad` — runs on navigation, before the route mounts. If the + * user is already known anonymous, redirect to `/login` carrying the + * intended destination as `?redirect=`. This is the fast path on a + * cold visit when the boot probe has already resolved. + * + * 2. The layout component's effect — runs on every status transition + * *after* mount. Catches the case where `beforeLoad` saw `'unknown'` + * or `'authenticating'` (boot probe still in flight) and let the + * route render; once the probe resolves to `'anonymous'`, this effect + * navigates to `/login`. Without this fallback the user is stuck on a + * "Loading…" screen forever after a hard reload. */ export const Route = createFileRoute('/_authed')({ beforeLoad: ({ location }) => { @@ -29,6 +34,17 @@ export const Route = createFileRoute('/_authed')({ function AuthedLayout() { const status = useAuthStore((s) => s.status); + const navigate = useNavigate(); + + useEffect(() => { + if (status === 'anonymous') { + void navigate({ + to: '/login', + search: { redirect: window.location.pathname + window.location.search }, + replace: true, + }); + } + }, [status, navigate]); if (status !== 'authenticated') { return (