diff --git a/examples/react/start-basic-netlify/src/components/DefaultCatchBoundary.tsx b/examples/react/start-basic-netlify/src/components/DefaultCatchBoundary.tsx new file mode 100644 index 00000000000..d26c21f3841 --- /dev/null +++ b/examples/react/start-basic-netlify/src/components/DefaultCatchBoundary.tsx @@ -0,0 +1,63 @@ +import { + ErrorComponent, + Link, + rootRouteId, + useMatch, + useRouter, +} from '@tanstack/react-router' +import type { ErrorComponentProps } from '@tanstack/react-router' + +/** + * Render a default error UI for a caught route error with controls to retry or navigate. + * + * Logs the provided error to the console and renders an ErrorComponent alongside buttons + * to invalidate the router (retry) and either navigate home or go back in history depending + * on whether the current match is the root route. + * + * @param error - The caught error provided to the catch boundary + * @returns A React element displaying the error and action controls to retry or navigate + */ +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter() + const isRoot = useMatch({ + strict: false, + select: (state) => state.id === rootRouteId, + }) + + console.error('DefaultCatchBoundary Error:', error) + + return ( +
+ +
+ + {isRoot ? ( + + Home + + ) : ( + { + e.preventDefault() + window.history.back() + }} + > + Go Back + + )} +
+
+ ) +} diff --git a/examples/react/start-basic-netlify/src/components/NotFound.tsx b/examples/react/start-basic-netlify/src/components/NotFound.tsx new file mode 100644 index 00000000000..6958915f2cc --- /dev/null +++ b/examples/react/start-basic-netlify/src/components/NotFound.tsx @@ -0,0 +1,31 @@ +import { Link } from '@tanstack/react-router' + +/** + * Render a simple "not found" view that shows an optional message and two navigation actions. + * + * @param children - Optional custom content to display instead of the default "The page you are looking for does not exist." message. + * @returns A React element containing the message area and two controls: a "Go back" button (calls history.back) and a "Start Over" link to the root path. + */ +export function NotFound({ children }: { children?: React.ReactNode }) { + return ( +
+
+ {children ||

The page you are looking for does not exist.

} +
+

+ + + Start Over + +

+
+ ) +} diff --git a/examples/react/start-basic-netlify/src/components/PostError.tsx b/examples/react/start-basic-netlify/src/components/PostError.tsx new file mode 100644 index 00000000000..3361507acf5 --- /dev/null +++ b/examples/react/start-basic-netlify/src/components/PostError.tsx @@ -0,0 +1,12 @@ +import { ErrorComponent } from '@tanstack/react-router' +import type { ErrorComponentProps } from '@tanstack/react-router' + +/** + * Displays the given routing error. + * + * @param error - The error object provided by the router for the failed route. + * @returns A JSX element that renders the error UI. + */ +export function PostErrorComponent({ error }: ErrorComponentProps) { + return +} diff --git a/examples/react/start-basic-netlify/src/components/UserError.tsx b/examples/react/start-basic-netlify/src/components/UserError.tsx new file mode 100644 index 00000000000..9926148916b --- /dev/null +++ b/examples/react/start-basic-netlify/src/components/UserError.tsx @@ -0,0 +1,12 @@ +import { ErrorComponent } from '@tanstack/react-router' +import type { ErrorComponentProps } from '@tanstack/react-router' + +/** + * Renders an error UI for a routing error. + * + * @param error - The error object produced during route handling to display to the user. + * @returns The rendered error UI element. + */ +export function UserErrorComponent({ error }: ErrorComponentProps) { + return +} diff --git a/examples/react/start-basic-netlify/src/router.tsx b/examples/react/start-basic-netlify/src/router.tsx new file mode 100644 index 00000000000..285a7d8bf0d --- /dev/null +++ b/examples/react/start-basic-netlify/src/router.tsx @@ -0,0 +1,20 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' +import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' +import { NotFound } from './components/NotFound' + +/** + * Create and return a configured TanStack React Router instance for the application. + * + * @returns A router configured with the application's `routeTree`, intent preloading, `DefaultCatchBoundary` as the default error component, `NotFound` as the default not-found component, and scroll restoration enabled. + */ +export function getRouter() { + const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + scrollRestoration: true, + }) + return router +} diff --git a/examples/react/start-basic-netlify/src/routes/__root.tsx b/examples/react/start-basic-netlify/src/routes/__root.tsx new file mode 100644 index 00000000000..0a2190e29a0 --- /dev/null +++ b/examples/react/start-basic-netlify/src/routes/__root.tsx @@ -0,0 +1,140 @@ +/// +import { + HeadContent, + Link, + 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' }, + ], + scripts: [ + { + src: '/customScript.js', + type: 'text/javascript', + }, + ], + }), + errorComponent: DefaultCatchBoundary, + notFoundComponent: () => , + shellComponent: RootDocument, +}) + +/** + * Render the application's HTML document shell with global navigation and injected head/scripts. + * + * Renders a complete HTML structure including HeadContent, a top navigation bar of internal links, + * a content outlet for `children`, the TanStack Router Devtools, and Scripts. + * + * @param children - Routed content to render inside the document body + * @returns The root HTML document element that wraps routed content and provides head, navigation, devtools, and scripts + */ +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + + + +
+ + Home + {' '} + + Posts + {' '} + + Users + {' '} + + Pathless Layout + {' '} + + Deferred + {' '} + + This Route Does Not Exist + +
+
+ {children} + + + + + ) +} diff --git a/examples/react/start-basic-netlify/src/routes/_pathlessLayout.tsx b/examples/react/start-basic-netlify/src/routes/_pathlessLayout.tsx new file mode 100644 index 00000000000..24c15ce0129 --- /dev/null +++ b/examples/react/start-basic-netlify/src/routes/_pathlessLayout.tsx @@ -0,0 +1,21 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_pathlessLayout')({ + component: LayoutComponent, +}) + +/** + * Layout component that renders a header and an Outlet for nested routes. + * + * @returns A JSX element containing a container with a top header and an for nested route content. + */ +function LayoutComponent() { + return ( +
+
I'm a layout
+
+ +
+
+ ) +} diff --git a/examples/react/start-basic-netlify/src/routes/_pathlessLayout/_nested-layout.tsx b/examples/react/start-basic-netlify/src/routes/_pathlessLayout/_nested-layout.tsx new file mode 100644 index 00000000000..9eac8d6c942 --- /dev/null +++ b/examples/react/start-basic-netlify/src/routes/_pathlessLayout/_nested-layout.tsx @@ -0,0 +1,39 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_pathlessLayout/_nested-layout')({ + component: LayoutComponent, +}) + +/** + * Renders a nested layout with a header, two navigation links (Route A and Route B), and an Outlet for nested route content. + * + * @returns The JSX element containing the layout header, navigation links with active styling, and an Outlet for child routes. + */ +function LayoutComponent() { + return ( +
+
I'm a nested layout
+
+ + Go to route A + + + Go to route B + +
+
+ +
+
+ ) +} diff --git a/examples/react/start-basic-netlify/src/routes/_pathlessLayout/_nested-layout/route-a.tsx b/examples/react/start-basic-netlify/src/routes/_pathlessLayout/_nested-layout/route-a.tsx new file mode 100644 index 00000000000..fd626c7f4da --- /dev/null +++ b/examples/react/start-basic-netlify/src/routes/_pathlessLayout/_nested-layout/route-a.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-a')( + { + component: LayoutAComponent, + }, +) + +/** + * Renders the layout A component showing "I'm A!". + * + * @returns A JSX element containing a div with the text "I'm A!". + */ +function LayoutAComponent() { + return
I'm A!
+} diff --git a/examples/react/start-basic-netlify/src/routes/_pathlessLayout/_nested-layout/route-b.tsx b/examples/react/start-basic-netlify/src/routes/_pathlessLayout/_nested-layout/route-b.tsx new file mode 100644 index 00000000000..4b71f9d5aec --- /dev/null +++ b/examples/react/start-basic-netlify/src/routes/_pathlessLayout/_nested-layout/route-b.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-b')( + { + component: LayoutBComponent, + }, +) + +/** + * Renders a simple layout component that displays "I'm B!". + * + * @returns A JSX element containing a `div` with the text "I'm B!". + */ +function LayoutBComponent() { + return
I'm B!
+} diff --git a/examples/react/start-basic-netlify/src/routes/deferred.tsx b/examples/react/start-basic-netlify/src/routes/deferred.tsx new file mode 100644 index 00000000000..a2bfab1a86a --- /dev/null +++ b/examples/react/start-basic-netlify/src/routes/deferred.tsx @@ -0,0 +1,71 @@ +import { Await, createFileRoute } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/react-start' +import { Suspense, useState } from 'react' + +const personServerFn = createServerFn({ method: 'GET' }) + .inputValidator((d: string) => d) + .handler(({ data: name }) => { + return { name, randomNumber: Math.floor(Math.random() * 100) } + }) + +const slowServerFn = createServerFn({ method: 'GET' }) + .inputValidator((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((r) => + setTimeout(() => r('Hello deferred!'), 2000), + ), + deferredPerson: slowServerFn({ data: 'Tanner Linsley' }), + person: await personServerFn({ data: 'John Doe' }), + } + }, + component: Deferred, +}) + +/** + * Renders the UI for the /deferred route: shows immediate server-loaded person data, two Suspense/Await regions for deferred person and string values, and a local counter with an increment button. + * + * @returns The component's rendered JSX containing: + * - an immediately displayed person entry, + * - a Suspense-wrapped Await for `deferredPerson` that renders when resolved, + * - a Suspense-wrapped Await for `deferredStuff` that renders when resolved, + * - a numeric `Count` display and a button that increments the count. + */ +function Deferred() { + const [count, setCount] = useState(0) + const { deferredStuff, deferredPerson, person } = Route.useLoaderData() + + return ( +
+
+ {person.name} - {person.randomNumber} +
+ Loading person...
}> + ( +
+ {data.name} - {data.randomNumber} +
+ )} + /> + + Loading stuff...}> +

{data}

} + /> +
+
Count: {count}
+
+ +
+ + ) +} diff --git a/examples/react/start-basic-netlify/src/routes/index.tsx b/examples/react/start-basic-netlify/src/routes/index.tsx new file mode 100644 index 00000000000..df2e1f0ee1a --- /dev/null +++ b/examples/react/start-basic-netlify/src/routes/index.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +/** + * Renders the home view containing a padded container with a welcome heading. + * + * @returns A React element that renders a div with a "Welcome Home!!!" heading. + */ +function Home() { + return ( +
+

Welcome Home!!!

+
+ ) +} diff --git a/examples/react/start-basic-netlify/src/routes/posts.$postId.tsx b/examples/react/start-basic-netlify/src/routes/posts.$postId.tsx new file mode 100644 index 00000000000..168735ff552 --- /dev/null +++ b/examples/react/start-basic-netlify/src/routes/posts.$postId.tsx @@ -0,0 +1,39 @@ +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 Post not found + }, +}) + +/** + * Render the loaded post's title, body, and a link to the post's deep view. + * + * @returns The JSX element displaying the post title, post body, and a "Deep View" link that navigates to the nested `/posts/$postId/deep` route. + */ +function PostComponent() { + const post = Route.useLoaderData() + + return ( +
+

{post.title}

+
{post.body}
+ + Deep View + +
+ ) +} diff --git a/examples/react/start-basic-netlify/src/routes/posts.index.tsx b/examples/react/start-basic-netlify/src/routes/posts.index.tsx new file mode 100644 index 00000000000..292ea71ac6d --- /dev/null +++ b/examples/react/start-basic-netlify/src/routes/posts.index.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts/')({ + component: PostsIndexComponent, +}) + +/** + * Renders a placeholder prompting the user to select a post. + * + * @returns A JSX element containing the text "Select a post." + */ +function PostsIndexComponent() { + return
Select a post.
+} diff --git a/examples/react/start-basic-netlify/src/routes/posts.tsx b/examples/react/start-basic-netlify/src/routes/posts.tsx new file mode 100644 index 00000000000..bcde6467fc2 --- /dev/null +++ b/examples/react/start-basic-netlify/src/routes/posts.tsx @@ -0,0 +1,47 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/react-router' +import { fetchPosts } from '../utils/posts' + +export const Route = createFileRoute('/posts')({ + loader: async () => fetchPosts(), + component: PostsComponent, +}) + +/** + * Render the posts index: a navigable list of posts and an outlet for nested routes. + * + * Renders loader-provided posts as links to `/posts/{postId}`, appends a placeholder + * item with id `"i-do-not-exist"`, truncates titles to 20 characters, and displays + * an Outlet for nested routes. + * + * @returns The component's JSX: a list of post links and an Outlet for nested content. + */ +function PostsComponent() { + const posts = Route.useLoaderData() + + return ( +
+
    + {[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( + (post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ) + }, + )} +
+
+ +
+ ) +} diff --git a/examples/react/start-basic-netlify/src/routes/posts_.$postId.deep.tsx b/examples/react/start-basic-netlify/src/routes/posts_.$postId.deep.tsx new file mode 100644 index 00000000000..6e20b99d898 --- /dev/null +++ b/examples/react/start-basic-netlify/src/routes/posts_.$postId.deep.tsx @@ -0,0 +1,37 @@ +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, +}) + +/** + * Display a detailed post view with navigation back to the posts list. + * + * Shows the post title and body using the data provided by the route loader, + * and renders a link to return to the "All Posts" page. + * + * @returns A JSX element rendering the post detail UI + */ +function PostDeepComponent() { + const post = Route.useLoaderData() + + return ( +
+ + ← All Posts + +

{post.title}

+
{post.body}
+
+ ) +} diff --git a/examples/react/start-basic-netlify/src/routes/users.$userId.tsx b/examples/react/start-basic-netlify/src/routes/users.$userId.tsx new file mode 100644 index 00000000000..50997ea5718 --- /dev/null +++ b/examples/react/start-basic-netlify/src/routes/users.$userId.tsx @@ -0,0 +1,39 @@ +import { createFileRoute } from '@tanstack/react-router' +import { NotFound } from 'src/components/NotFound' +import { UserErrorComponent } from 'src/components/UserError' +import { fetchUser } from '../utils/users' + +export const Route = createFileRoute('/users/$userId')({ + loader: ({ params: { userId } }) => fetchUser({ data: userId }), + errorComponent: UserErrorComponent, + component: UserComponent, + notFoundComponent: () => { + return User not found + }, +}) + +/** + * Displays a user's name, email, and a link to view the user's data as JSON. + * + * Renders the loaded user from route loader data with the name as a bold, underlined heading, the email in smaller text, and a "View as JSON" link to `/api/users/{id}`. + * + * @returns A JSX element rendering the user's name, email, and JSON view link + */ +function UserComponent() { + const user = Route.useLoaderData() + + return ( +
+

{user.name}

+
{user.email}
+ +
+ ) +} diff --git a/examples/react/start-basic-netlify/src/routes/users.index.tsx b/examples/react/start-basic-netlify/src/routes/users.index.tsx new file mode 100644 index 00000000000..a8a45e2264e --- /dev/null +++ b/examples/react/start-basic-netlify/src/routes/users.index.tsx @@ -0,0 +1,24 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/users/')({ + component: UsersIndexComponent, +}) + +/** + * Renders the users index view with a prompt and a link to the JSON users API. + * + * @returns A React element containing a message and an anchor linking to `/api/users`. + */ +function UsersIndexComponent() { + return ( +
+ Select a user or{' '} + + view as JSON + +
+ ) +} diff --git a/examples/react/start-basic-netlify/src/routes/users.tsx b/examples/react/start-basic-netlify/src/routes/users.tsx new file mode 100644 index 00000000000..81ceeaf1464 --- /dev/null +++ b/examples/react/start-basic-netlify/src/routes/users.tsx @@ -0,0 +1,47 @@ +import { Link, Outlet, createFileRoute } from '@tanstack/react-router' +import { fetchUsers } from '../utils/users' + +export const Route = createFileRoute('/users')({ + loader: () => fetchUsers(), + component: UsersComponent, +}) + +/** + * Renders the users list with navigation links and a nested-route outlet. + * + * The component reads loader-provided user data and renders each user as a link to `/users/$userId`. + * It appends a hard-coded "Non-existent User" item to the list. Below the list it renders an `` for nested routes. + * + * @returns A JSX element containing the user list with links and an Outlet for nested route content. + */ +function UsersComponent() { + const users = Route.useLoaderData() + + return ( +
+
    + {[ + ...users, + { id: 'i-do-not-exist', name: 'Non-existent User', email: '' }, + ].map((user) => { + return ( +
  • + +
    {user.name}
    + +
  • + ) + })} +
+
+ +
+ ) +}