fix(routing): redirect anonymous users from protected routes + devtools rename
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.
This commit is contained in:
@@ -11,3 +11,7 @@ pnpm-lock.yaml
|
|||||||
public
|
public
|
||||||
src/routeTree.gen.ts
|
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
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -34,8 +34,8 @@
|
|||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@tailwindcss/vite": "^4.2.4",
|
"@tailwindcss/vite": "^4.2.4",
|
||||||
"@tanstack/react-query-devtools": "^5.100.8",
|
"@tanstack/react-query-devtools": "^5.100.8",
|
||||||
|
"@tanstack/react-router-devtools": "^1.166.13",
|
||||||
"@tanstack/router-cli": "^1.166.40",
|
"@tanstack/router-cli": "^1.166.40",
|
||||||
"@tanstack/router-devtools": "^1.166.13",
|
|
||||||
"@tanstack/router-plugin": "^1.167.32",
|
"@tanstack/router-plugin": "^1.167.32",
|
||||||
"@types/node": "^24.12.2",
|
"@types/node": "^24.12.2",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
|
|||||||
Generated
+3
-28
@@ -60,12 +60,12 @@ importers:
|
|||||||
'@tanstack/react-query-devtools':
|
'@tanstack/react-query-devtools':
|
||||||
specifier: ^5.100.8
|
specifier: ^5.100.8
|
||||||
version: 5.100.8(@tanstack/react-query@5.100.8(react@19.2.5))(react@19.2.5)
|
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':
|
'@tanstack/router-cli':
|
||||||
specifier: ^1.166.40
|
specifier: ^1.166.40
|
||||||
version: 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':
|
'@tanstack/router-plugin':
|
||||||
specifier: ^1.167.32
|
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))
|
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:
|
csstype:
|
||||||
optional: true
|
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':
|
'@tanstack/router-generator@1.166.39':
|
||||||
resolution: {integrity: sha512-j2OW/UvpjM/DT9tHVmuhWW1k6UOezTRrPqBPZFFmIth0fY7iTPqK+Erqpo8r5yGTRGCbMvOS4sL3H2MldnIZew==}
|
resolution: {integrity: sha512-j2OW/UvpjM/DT9tHVmuhWW1k6UOezTRrPqBPZFFmIth0fY7iTPqK+Erqpo8r5yGTRGCbMvOS4sL3H2MldnIZew==}
|
||||||
engines: {node: '>=20.19'}
|
engines: {node: '>=20.19'}
|
||||||
@@ -3435,19 +3423,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
csstype: 3.2.3
|
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':
|
'@tanstack/router-generator@1.166.39':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.0
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { queryClient } from '@/lib/query-client';
|
|||||||
// `import.meta.env.DEV`.
|
// `import.meta.env.DEV`.
|
||||||
const TanStackRouterDevtools = import.meta.env.DEV
|
const TanStackRouterDevtools = import.meta.env.DEV
|
||||||
? lazy(() =>
|
? lazy(() =>
|
||||||
import('@tanstack/router-devtools').then((mod) => ({
|
import('@tanstack/react-router-devtools').then((mod) => ({
|
||||||
default: mod.TanStackRouterDevtools,
|
default: mod.TanStackRouterDevtools,
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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';
|
import { useAuthStore } from '@/auth';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pathless layout for all protected routes.
|
* Pathless layout for all protected routes.
|
||||||
*
|
*
|
||||||
* `beforeLoad` is the canonical TanStack Router gate — runs before any
|
* Two complementary gates:
|
||||||
* 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=`.
|
|
||||||
*
|
*
|
||||||
* If status is `'unknown'` or `'authenticating'` (boot probe still in
|
* 1. `beforeLoad` — runs on navigation, before the route mounts. If the
|
||||||
* flight), let the route render — the layout component below shows a
|
* user is already known anonymous, redirect to `/login` carrying the
|
||||||
* loading placeholder until the probe resolves. Without that, the user
|
* intended destination as `?redirect=`. This is the fast path on a
|
||||||
* gets an unwanted login flash on every hard reload.
|
* 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')({
|
export const Route = createFileRoute('/_authed')({
|
||||||
beforeLoad: ({ location }) => {
|
beforeLoad: ({ location }) => {
|
||||||
@@ -29,6 +34,17 @@ export const Route = createFileRoute('/_authed')({
|
|||||||
|
|
||||||
function AuthedLayout() {
|
function AuthedLayout() {
|
||||||
const status = useAuthStore((s) => s.status);
|
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') {
|
if (status !== 'authenticated') {
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user