feat(tanstack): initialize TanStack project with routing, API, and error handling components

This commit is contained in:
Mauricio Siu
2025-04-06 03:53:25 -06:00
parent a0636d8083
commit 0d6ff904a8
51 changed files with 7387 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
import {
HeadContent,
Link,
Outlet,
Scripts,
createRootRoute,
} from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
import * as React from 'react'
import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
import { NotFound } from '~/components/NotFound'
import appCss from '~/styles/app.css?url'
import { seo } from '~/utils/seo'
export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
...seo({
title:
'TanStack Start | Type-Safe, Client-First, Full-Stack React Framework',
description: `TanStack Start is a type-safe, client-first, full-stack React framework. `,
}),
],
links: [
{ rel: 'stylesheet', href: appCss },
{
rel: 'apple-touch-icon',
sizes: '180x180',
href: '/apple-touch-icon.png',
},
{
rel: 'icon',
type: 'image/png',
sizes: '32x32',
href: '/favicon-32x32.png',
},
{
rel: 'icon',
type: 'image/png',
sizes: '16x16',
href: '/favicon-16x16.png',
},
{ rel: 'manifest', href: '/site.webmanifest', color: '#fffff' },
{ rel: 'icon', href: '/favicon.ico' },
],
}),
errorComponent: (props) => {
return (
<RootDocument>
<DefaultCatchBoundary {...props} />
</RootDocument>
)
},
notFoundComponent: () => <NotFound />,
component: RootComponent,
})
function RootComponent() {
return (
<RootDocument>
<Outlet />
</RootDocument>
)
}
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
<div className="p-2 flex gap-2 text-lg">
<Link
to="/"
activeProps={{
className: 'font-bold',
}}
activeOptions={{ exact: true }}
>
Home
</Link>{' '}
<Link
to="/posts"
activeProps={{
className: 'font-bold',
}}
>
Posts
</Link>{' '}
<Link
to="/users"
activeProps={{
className: 'font-bold',
}}
>
Users
</Link>{' '}
<Link
to="/route-a"
activeProps={{
className: 'font-bold',
}}
>
Pathless Layout
</Link>{' '}
<Link
to="/deferred"
activeProps={{
className: 'font-bold',
}}
>
Deferred
</Link>{' '}
<Link
// @ts-expect-error
to="/this-route-does-not-exist"
activeProps={{
className: 'font-bold',
}}
>
This Route Does Not Exist
</Link>
</div>
<hr />
{children}
<TanStackRouterDevtools position="bottom-right" />
<Scripts />
</body>
</html>
)
}

View File

@@ -0,0 +1,16 @@
import { Outlet, createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_pathlessLayout')({
component: LayoutComponent,
})
function LayoutComponent() {
return (
<div className="p-2">
<div className="border-b">I'm a layout</div>
<div>
<Outlet />
</div>
</div>
)
}

View File

@@ -0,0 +1,34 @@
import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_pathlessLayout/_nested-layout')({
component: LayoutComponent,
})
function LayoutComponent() {
return (
<div>
<div>I'm a nested layout</div>
<div className="flex gap-2 border-b">
<Link
to="/route-a"
activeProps={{
className: 'font-bold',
}}
>
Go to route A
</Link>
<Link
to="/route-b"
activeProps={{
className: 'font-bold',
}}
>
Go to route B
</Link>
</div>
<div>
<Outlet />
</div>
</div>
)
}

View File

@@ -0,0 +1,11 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-a')(
{
component: LayoutAComponent,
},
)
function LayoutAComponent() {
return <div>I'm A!</div>
}

View File

@@ -0,0 +1,11 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-b')(
{
component: LayoutBComponent,
},
)
function LayoutBComponent() {
return <div>I'm B!</div>
}

View File

@@ -0,0 +1,24 @@
import { json } from '@tanstack/react-start'
import { createAPIFileRoute } from '@tanstack/react-start/api'
import axios from 'redaxios'
import type { User } from '../../utils/users'
export const APIRoute = createAPIFileRoute('/api/users/$id')({
GET: async ({ request, params }) => {
console.info(`Fetching users by id=${params.id}... @`, request.url)
try {
const res = await axios.get<User>(
'https://jsonplaceholder.typicode.com/users/' + params.id,
)
return json({
id: res.data.id,
name: res.data.name,
email: res.data.email,
})
} catch (e) {
console.error(e)
return json({ error: 'User not found' }, { status: 404 })
}
},
})

View File

@@ -0,0 +1,17 @@
import { json } from '@tanstack/react-start'
import { createAPIFileRoute } from '@tanstack/react-start/api'
import axios from 'redaxios'
import type { User } from '../../utils/users'
export const APIRoute = createAPIFileRoute('/api/users')({
GET: async ({ request }) => {
console.info('Fetching users... @', request.url)
const res = await axios.get<Array<User>>(
'https://jsonplaceholder.typicode.com/users',
)
const list = res.data.slice(0, 10)
return json(list.map((u) => ({ id: u.id, name: u.name, email: u.email })))
},
})

View File

@@ -0,0 +1,62 @@
import { Await, createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import { Suspense, useState } from 'react'
const personServerFn = createServerFn({ method: 'GET' })
.validator((d: string) => d)
.handler(({ data: name }) => {
return { name, randomNumber: Math.floor(Math.random() * 100) }
})
const slowServerFn = createServerFn({ method: 'GET' })
.validator((d: string) => d)
.handler(async ({ data: name }) => {
await new Promise((r) => setTimeout(r, 1000))
return { name, randomNumber: Math.floor(Math.random() * 100) }
})
export const Route = createFileRoute('/deferred')({
loader: async () => {
return {
deferredStuff: new Promise<string>((r) =>
setTimeout(() => r('Hello deferred!'), 2000),
),
deferredPerson: slowServerFn({ data: 'Tanner Linsley' }),
person: await personServerFn({ data: 'John Doe' }),
}
},
component: Deferred,
})
function Deferred() {
const [count, setCount] = useState(0)
const { deferredStuff, deferredPerson, person } = Route.useLoaderData()
return (
<div className="p-2">
<div data-testid="regular-person">
{person.name} - {person.randomNumber}
</div>
<Suspense fallback={<div>Loading person...</div>}>
<Await
promise={deferredPerson}
children={(data) => (
<div data-testid="deferred-person">
{data.name} - {data.randomNumber}
</div>
)}
/>
</Suspense>
<Suspense fallback={<div>Loading stuff...</div>}>
<Await
promise={deferredStuff}
children={(data) => <h3 data-testid="deferred-stuff">{data}</h3>}
/>
</Suspense>
<div>Count: {count}</div>
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,13 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: Home,
})
function Home() {
return (
<div className="p-2">
<h3>Welcome Home!!!</h3>
</div>
)
}

View File

@@ -0,0 +1,34 @@
import { Link, createFileRoute } from '@tanstack/react-router'
import { fetchPost } from '../utils/posts'
import { NotFound } from '~/components/NotFound'
import { PostErrorComponent } from '~/components/PostError'
export const Route = createFileRoute('/posts/$postId')({
loader: ({ params: { postId } }) => fetchPost({ data: postId }),
errorComponent: PostErrorComponent,
component: PostComponent,
notFoundComponent: () => {
return <NotFound>Post not found</NotFound>
},
})
function PostComponent() {
const post = Route.useLoaderData()
return (
<div className="space-y-2">
<h4 className="text-xl font-bold underline">{post.title}</h4>
<div className="text-sm">{post.body}</div>
<Link
to="/posts/$postId/deep"
params={{
postId: post.id,
}}
activeProps={{ className: 'text-black font-bold' }}
className="block py-1 text-blue-800 hover:text-blue-600"
>
Deep View
</Link>
</div>
)
}

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/')({
component: PostsIndexComponent,
})
function PostsIndexComponent() {
return <div>Select a post.</div>
}

View File

@@ -0,0 +1,38 @@
import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
import { fetchPosts } from '../utils/posts'
export const Route = createFileRoute('/posts')({
loader: async () => fetchPosts(),
component: PostsLayoutComponent,
})
function PostsLayoutComponent() {
const posts = Route.useLoaderData()
return (
<div className="p-2 flex gap-2">
<ul className="list-disc pl-4">
{[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map(
(post) => {
return (
<li key={post.id} className="whitespace-nowrap">
<Link
to="/posts/$postId"
params={{
postId: post.id,
}}
className="block py-1 text-blue-800 hover:text-blue-600"
activeProps={{ className: 'text-black font-bold' }}
>
<div>{post.title.substring(0, 20)}</div>
</Link>
</li>
)
},
)}
</ul>
<hr />
<Outlet />
</div>
)
}

View File

@@ -0,0 +1,29 @@
import { Link, createFileRoute } from '@tanstack/react-router'
import { fetchPost } from '../utils/posts'
import { PostErrorComponent } from '~/components/PostError'
export const Route = createFileRoute('/posts_/$postId/deep')({
loader: async ({ params: { postId } }) =>
fetchPost({
data: postId,
}),
errorComponent: PostErrorComponent,
component: PostDeepComponent,
})
function PostDeepComponent() {
const post = Route.useLoaderData()
return (
<div className="p-2 space-y-2">
<Link
to="/posts"
className="block py-1 text-blue-800 hover:text-blue-600"
>
All Posts
</Link>
<h4 className="text-xl font-bold underline">{post.title}</h4>
<div className="text-sm">{post.body}</div>
</div>
)
}

View File

@@ -0,0 +1,9 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/redirect')({
beforeLoad: async () => {
throw redirect({
to: '/posts',
})
},
})

View File

@@ -0,0 +1,33 @@
import { createFileRoute } from '@tanstack/react-router'
import axios from 'redaxios'
import type { User } from '~/utils/users'
import { DEPLOY_URL } from '~/utils/users'
import { NotFound } from '~/components/NotFound'
import { UserErrorComponent } from '~/components/UserError'
export const Route = createFileRoute('/users/$userId')({
loader: async ({ params: { userId } }) => {
return await axios
.get<User>(DEPLOY_URL + '/api/users/' + userId)
.then((r) => r.data)
.catch(() => {
throw new Error('Failed to fetch user')
})
},
errorComponent: UserErrorComponent,
component: UserComponent,
notFoundComponent: () => {
return <NotFound>User not found</NotFound>
},
})
function UserComponent() {
const user = Route.useLoaderData()
return (
<div className="space-y-2">
<h4 className="text-xl font-bold underline">{user.name}</h4>
<div className="text-sm">{user.email}</div>
</div>
)
}

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/users/')({
component: UsersIndexComponent,
})
function UsersIndexComponent() {
return <div>Select a user.</div>
}

View File

@@ -0,0 +1,48 @@
import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
import axios from 'redaxios'
import { DEPLOY_URL } from '../utils/users'
import type { User } from '../utils/users'
export const Route = createFileRoute('/users')({
loader: async () => {
return await axios
.get<Array<User>>(DEPLOY_URL + '/api/users')
.then((r) => r.data)
.catch(() => {
throw new Error('Failed to fetch users')
})
},
component: UsersLayoutComponent,
})
function UsersLayoutComponent() {
const users = Route.useLoaderData()
return (
<div className="p-2 flex gap-2">
<ul className="list-disc pl-4">
{[
...users,
{ id: 'i-do-not-exist', name: 'Non-existent User', email: '' },
].map((user) => {
return (
<li key={user.id} className="whitespace-nowrap">
<Link
to="/users/$userId"
params={{
userId: String(user.id),
}}
className="block py-1 text-blue-800 hover:text-blue-600"
activeProps={{ className: 'text-black font-bold' }}
>
<div>{user.name}</div>
</Link>
</li>
)
})}
</ul>
<hr />
<Outlet />
</div>
)
}