Skip to content

Commit 1f20d6e

Browse files
KyleAMathewsclaudeautofix-ci[bot]
authored
Add default 404 page and loading state to project example (#1257)
* fix(examples): handle undefined project in React projects example Add a guard in the ProjectPage component to handle the case where the project or user membership data hasn't loaded yet, preventing the "Cannot read properties of undefined (reading 'name')" crash. Also add a defaultNotFoundComponent to the router to handle invalid routes gracefully instead of throwing an unconfigured notFoundError. Fixes #889 https://claude.ai/code/session_01QJP1o2CKx7s9iLPNEkwcuP * refactor(examples): validate project in loader and extract NotFound component Throw notFound() from the route loader when the project ID is invalid or doesn't exist, instead of showing a misleading "Loading..." message. Extract NotFound into a reusable component using Link for client-side navigation, consistent with the todo and offline-transactions examples. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(examples): fix auth routing and move project creation to loader - Move auth API route to catch-all (api/auth/$.ts) so Better Auth sub-paths like /api/auth/sign-up/email are handled correctly - Add baseURL and port 5174 to Better Auth trusted origins - Move default project creation from useEffect into beforeLoad so data is ready before first render, eliminating race conditions - Wait for project to persist before redirecting to get server-assigned ID - Remove unused useEffect and isLoading from authenticated layout Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: apply automated fixes --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 4ff3da5 commit 1f20d6e

8 files changed

Lines changed: 90 additions & 79 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Link } from '@tanstack/react-router'
2+
3+
export function NotFound() {
4+
return (
5+
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
6+
<div className="text-center">
7+
<h1 className="text-4xl font-bold text-gray-800 mb-4">
8+
Page Not Found
9+
</h1>
10+
<p className="text-gray-600 mb-6">
11+
The page you are looking for does not exist.
12+
</p>
13+
<Link
14+
to="/"
15+
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
16+
>
17+
Go Home
18+
</Link>
19+
</div>
20+
</div>
21+
)
22+
}

examples/react/projects/src/lib/auth.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { db } from '@/db/connection' // your drizzle instance
44
import * as schema from '@/db/auth-schema'
55

66
export const auth = betterAuth({
7+
baseURL: process.env.BETTER_AUTH_BASE_URL || 'http://localhost:5174',
78
database: drizzleAdapter(db, {
89
provider: 'pg',
910
usePlural: true,
@@ -16,7 +17,5 @@ export const auth = betterAuth({
1617
disableSignUp: process.env.NODE_ENV === 'production',
1718
minPasswordLength: process.env.NODE_ENV === 'production' ? 8 : 1,
1819
},
19-
trustedOrigins: [
20-
'http://localhost:5173', // Vite dev server
21-
],
20+
trustedOrigins: ['http://localhost:5173', 'http://localhost:5174'],
2221
})

examples/react/projects/src/routeTree.gen.ts

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import { Route as rootRouteImport } from './routes/__root'
1212
import { Route as LoginRouteImport } from './routes/login'
1313
import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
1414
import { Route as AuthenticatedIndexRouteImport } from './routes/_authenticated/index'
15-
import { Route as ApiAuthRouteImport } from './routes/api/auth'
1615
import { Route as ApiTrpcSplatRouteImport } from './routes/api/trpc/$'
16+
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
1717
import { Route as AuthenticatedProjectProjectIdRouteImport } from './routes/_authenticated/project/$projectId'
1818

1919
const LoginRoute = LoginRouteImport.update({
@@ -30,16 +30,16 @@ const AuthenticatedIndexRoute = AuthenticatedIndexRouteImport.update({
3030
path: '/',
3131
getParentRoute: () => AuthenticatedRoute,
3232
} as any)
33-
const ApiAuthRoute = ApiAuthRouteImport.update({
34-
id: '/api/auth',
35-
path: '/api/auth',
36-
getParentRoute: () => rootRouteImport,
37-
} as any)
3833
const ApiTrpcSplatRoute = ApiTrpcSplatRouteImport.update({
3934
id: '/api/trpc/$',
4035
path: '/api/trpc/$',
4136
getParentRoute: () => rootRouteImport,
4237
} as any)
38+
const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
39+
id: '/api/auth/$',
40+
path: '/api/auth/$',
41+
getParentRoute: () => rootRouteImport,
42+
} as any)
4343
const AuthenticatedProjectProjectIdRoute =
4444
AuthenticatedProjectProjectIdRouteImport.update({
4545
id: '/project/$projectId',
@@ -48,52 +48,52 @@ const AuthenticatedProjectProjectIdRoute =
4848
} as any)
4949

5050
export interface FileRoutesByFullPath {
51-
'/login': typeof LoginRoute
52-
'/api/auth': typeof ApiAuthRoute
5351
'/': typeof AuthenticatedIndexRoute
52+
'/login': typeof LoginRoute
5453
'/project/$projectId': typeof AuthenticatedProjectProjectIdRoute
54+
'/api/auth/$': typeof ApiAuthSplatRoute
5555
'/api/trpc/$': typeof ApiTrpcSplatRoute
5656
}
5757
export interface FileRoutesByTo {
5858
'/login': typeof LoginRoute
59-
'/api/auth': typeof ApiAuthRoute
6059
'/': typeof AuthenticatedIndexRoute
6160
'/project/$projectId': typeof AuthenticatedProjectProjectIdRoute
61+
'/api/auth/$': typeof ApiAuthSplatRoute
6262
'/api/trpc/$': typeof ApiTrpcSplatRoute
6363
}
6464
export interface FileRoutesById {
6565
__root__: typeof rootRouteImport
6666
'/_authenticated': typeof AuthenticatedRouteWithChildren
6767
'/login': typeof LoginRoute
68-
'/api/auth': typeof ApiAuthRoute
6968
'/_authenticated/': typeof AuthenticatedIndexRoute
7069
'/_authenticated/project/$projectId': typeof AuthenticatedProjectProjectIdRoute
70+
'/api/auth/$': typeof ApiAuthSplatRoute
7171
'/api/trpc/$': typeof ApiTrpcSplatRoute
7272
}
7373
export interface FileRouteTypes {
7474
fileRoutesByFullPath: FileRoutesByFullPath
7575
fullPaths:
76-
| '/login'
77-
| '/api/auth'
7876
| '/'
77+
| '/login'
7978
| '/project/$projectId'
79+
| '/api/auth/$'
8080
| '/api/trpc/$'
8181
fileRoutesByTo: FileRoutesByTo
82-
to: '/login' | '/api/auth' | '/' | '/project/$projectId' | '/api/trpc/$'
82+
to: '/login' | '/' | '/project/$projectId' | '/api/auth/$' | '/api/trpc/$'
8383
id:
8484
| '__root__'
8585
| '/_authenticated'
8686
| '/login'
87-
| '/api/auth'
8887
| '/_authenticated/'
8988
| '/_authenticated/project/$projectId'
89+
| '/api/auth/$'
9090
| '/api/trpc/$'
9191
fileRoutesById: FileRoutesById
9292
}
9393
export interface RootRouteChildren {
9494
AuthenticatedRoute: typeof AuthenticatedRouteWithChildren
9595
LoginRoute: typeof LoginRoute
96-
ApiAuthRoute: typeof ApiAuthRoute
96+
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
9797
ApiTrpcSplatRoute: typeof ApiTrpcSplatRoute
9898
}
9999

@@ -109,7 +109,7 @@ declare module '@tanstack/react-router' {
109109
'/_authenticated': {
110110
id: '/_authenticated'
111111
path: ''
112-
fullPath: ''
112+
fullPath: '/'
113113
preLoaderRoute: typeof AuthenticatedRouteImport
114114
parentRoute: typeof rootRouteImport
115115
}
@@ -120,20 +120,20 @@ declare module '@tanstack/react-router' {
120120
preLoaderRoute: typeof AuthenticatedIndexRouteImport
121121
parentRoute: typeof AuthenticatedRoute
122122
}
123-
'/api/auth': {
124-
id: '/api/auth'
125-
path: '/api/auth'
126-
fullPath: '/api/auth'
127-
preLoaderRoute: typeof ApiAuthRouteImport
128-
parentRoute: typeof rootRouteImport
129-
}
130123
'/api/trpc/$': {
131124
id: '/api/trpc/$'
132125
path: '/api/trpc/$'
133126
fullPath: '/api/trpc/$'
134127
preLoaderRoute: typeof ApiTrpcSplatRouteImport
135128
parentRoute: typeof rootRouteImport
136129
}
130+
'/api/auth/$': {
131+
id: '/api/auth/$'
132+
path: '/api/auth/$'
133+
fullPath: '/api/auth/$'
134+
preLoaderRoute: typeof ApiAuthSplatRouteImport
135+
parentRoute: typeof rootRouteImport
136+
}
137137
'/_authenticated/project/$projectId': {
138138
id: '/_authenticated/project/$projectId'
139139
path: '/project/$projectId'
@@ -161,7 +161,7 @@ const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
161161
const rootRouteChildren: RootRouteChildren = {
162162
AuthenticatedRoute: AuthenticatedRouteWithChildren,
163163
LoginRoute: LoginRoute,
164-
ApiAuthRoute: ApiAuthRoute,
164+
ApiAuthSplatRoute: ApiAuthSplatRoute,
165165
ApiTrpcSplatRoute: ApiTrpcSplatRoute,
166166
}
167167
export const routeTree = rootRouteImport

examples/react/projects/src/router.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createRouter as createTanstackRouter } from '@tanstack/react-router'
22

33
// Import the generated route tree
44
import { routeTree } from './routeTree.gen'
5+
import { NotFound } from './components/NotFound'
56

67
import './styles.css'
78

@@ -11,5 +12,6 @@ export function getRouter() {
1112
routeTree,
1213
scrollRestoration: true,
1314
defaultPreloadStaleTime: 0,
15+
defaultNotFoundComponent: NotFound,
1416
})
1517
}

examples/react/projects/src/routes/_authenticated.tsx

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from 'react'
1+
import { useState } from 'react'
22
import {
33
Link,
44
Outlet,
@@ -20,23 +20,7 @@ function AuthenticatedLayout() {
2020
const [showNewProjectForm, setShowNewProjectForm] = useState(false)
2121
const [newProjectName, setNewProjectName] = useState(``)
2222

23-
const { data: projects, isLoading } = useLiveQuery((q) =>
24-
q.from({ projectCollection })
25-
)
26-
27-
useEffect(() => {
28-
if (session && projects.length === 0 && !isLoading) {
29-
projectCollection.insert({
30-
id: Math.floor(Math.random() * 100000),
31-
name: `Default`,
32-
description: `Default project`,
33-
owner_id: session.user.id,
34-
shared_user_ids: [],
35-
created_at: new Date(),
36-
updated_at: new Date(),
37-
})
38-
}
39-
}, [session, projects, isLoading])
23+
const { data: projects } = useLiveQuery((q) => q.from({ projectCollection }))
4024

4125
const handleLogout = async () => {
4226
await authClient.signOut()
Lines changed: 26 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,49 @@
1-
import { useEffect } from 'react'
2-
import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router'
3-
import { useLiveQuery } from '@tanstack/react-db'
1+
import { createFileRoute, redirect } from '@tanstack/react-router'
42
import { projectCollection, todoCollection } from '@/lib/collections'
53
import { authClient } from '@/lib/auth-client'
64

75
export const Route = createFileRoute(`/_authenticated/`)({
8-
component: IndexRedirect,
6+
component: () => null,
97
ssr: false,
108
beforeLoad: async () => {
119
const res = await authClient.getSession()
1210
if (!res.data?.session) {
1311
throw redirect({
1412
to: `/login`,
1513
search: {
16-
// Use the current location to power a redirect after login
17-
// (Do not use `router.state.resolvedLocation` as it can
18-
// potentially lag behind the actual current location)
1914
redirect: location.href,
2015
},
2116
})
2217
}
23-
},
24-
loader: async () => {
18+
2519
await projectCollection.preload()
2620
await todoCollection.preload()
2721

28-
return null
29-
},
30-
})
31-
32-
function IndexRedirect() {
33-
const navigate = useNavigate()
34-
const { data: projects } = useLiveQuery((q) => q.from({ projectCollection }))
35-
36-
useEffect(() => {
37-
if (projects.length > 0) {
38-
const firstProject = projects[0]
39-
navigate({
22+
const projects = projectCollection.toArray
23+
if (projects.length === 0) {
24+
const id = Math.floor(Math.random() * 100000)
25+
const tx = projectCollection.insert({
26+
id,
27+
name: `Default`,
28+
description: `Default project`,
29+
owner_id: res.data.user.id,
30+
shared_user_ids: [],
31+
created_at: new Date(),
32+
updated_at: new Date(),
33+
})
34+
await tx.isPersisted.promise
35+
const serverProjectId = projectCollection.toArray[0].id
36+
throw redirect({
4037
to: `/project/$projectId`,
41-
params: { projectId: firstProject.id.toString() },
38+
params: { projectId: serverProjectId.toString() },
4239
replace: true,
4340
})
4441
}
45-
}, [projects, navigate])
4642

47-
return (
48-
<div className="p-6">
49-
<div className="text-center">
50-
<p className="text-gray-500">Loading projects...</p>
51-
</div>
52-
</div>
53-
)
54-
}
43+
throw redirect({
44+
to: `/project/$projectId`,
45+
params: { projectId: projects[0].id.toString() },
46+
replace: true,
47+
})
48+
},
49+
})

examples/react/projects/src/routes/_authenticated/project/$projectId.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createFileRoute } from '@tanstack/react-router'
1+
import { createFileRoute, notFound } from '@tanstack/react-router'
22
import { eq, useLiveQuery } from '@tanstack/react-db'
33
import { useState } from 'react'
44
import type { Todo } from '@/db/schema'
@@ -12,9 +12,13 @@ import {
1212
export const Route = createFileRoute(`/_authenticated/project/$projectId`)({
1313
component: ProjectPage,
1414
ssr: false,
15-
loader: async () => {
15+
loader: async ({ params }) => {
1616
await projectCollection.preload()
1717
await todoCollection.preload()
18+
const projectId = parseInt(params.projectId, 10)
19+
if (isNaN(projectId) || !projectCollection.has(projectId)) {
20+
throw notFound()
21+
}
1822
return null
1923
},
2024
})
@@ -83,6 +87,11 @@ function ProjectPage() {
8387
todoCollection.delete(id)
8488
}
8589

90+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
91+
if (!project || !usersInProject) {
92+
return null
93+
}
94+
8695
return (
8796
<div className="p-6">
8897
<div className="max-w-2xl mx-auto">

examples/react/projects/src/routes/api/auth.ts renamed to examples/react/projects/src/routes/api/auth/$.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const serve = ({ request }: { request: Request }) => {
55
return auth.handler(request)
66
}
77

8-
export const Route = createFileRoute(`/api/auth`)({
8+
export const Route = createFileRoute(`/api/auth/$`)({
99
server: {
1010
handlers: {
1111
GET: serve,

0 commit comments

Comments
 (0)