diff --git a/docs/router/config.json b/docs/router/config.json index 4758fb53fe6..5dfd26414ba 100644 --- a/docs/router/config.json +++ b/docs/router/config.json @@ -645,6 +645,10 @@ { "label": "Framer Motion", "to": "framework/solid/examples/with-framer-motion" + }, + { + "label": "With tRPC", + "to": "framework/solid/examples/with-trpc" } ] } diff --git a/examples/solid/with-trpc/.gitignore b/examples/solid/with-trpc/.gitignore new file mode 100644 index 00000000000..ca63f498851 --- /dev/null +++ b/examples/solid/with-trpc/.gitignore @@ -0,0 +1,18 @@ +node_modules +package-lock.json +yarn.lock + +.DS_Store +.cache +.env +.vercel +.output +/build/ +/api/ +/server/build +/public/build# Sentry Config File +.env.sentry-build-plugin +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/examples/solid/with-trpc/.vscode/settings.json b/examples/solid/with-trpc/.vscode/settings.json new file mode 100644 index 00000000000..00b5278e580 --- /dev/null +++ b/examples/solid/with-trpc/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "search.exclude": { + "**/routeTree.gen.ts": true + }, + "files.readonlyInclude": { + "**/routeTree.gen.ts": true + } +} diff --git a/examples/solid/with-trpc/README.md b/examples/solid/with-trpc/README.md new file mode 100644 index 00000000000..115199d292c --- /dev/null +++ b/examples/solid/with-trpc/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` or `yarn` +- `npm start` or `yarn start` diff --git a/examples/solid/with-trpc/index.html b/examples/solid/with-trpc/index.html new file mode 100644 index 00000000000..ba530a2c7a9 --- /dev/null +++ b/examples/solid/with-trpc/index.html @@ -0,0 +1,12 @@ + + + + + + Vite App + + +
+ + + diff --git a/examples/solid/with-trpc/package.json b/examples/solid/with-trpc/package.json new file mode 100644 index 00000000000..bc00142dcba --- /dev/null +++ b/examples/solid/with-trpc/package.json @@ -0,0 +1,32 @@ +{ + "name": "tanstack-router-solid-example-with-trpc", + "private": true, + "type": "module", + "scripts": { + "dev": "pnpm tsx ./src/server/server.ts --watch", + "build": "pnpm run build:server && pnpm run build:client", + "build:client": "vite build && tsc --noEmit", + "build:server": "vite build --mode server", + "start": "NODE_ENV=production node dist/server/server.js" + }, + "dependencies": { + "@tailwindcss/postcss": "^4.1.15", + "@tanstack/solid-router": "^1.135.2", + "@tanstack/solid-router-devtools": "^1.135.2", + "@tanstack/router-plugin": "^1.135.2", + "@trpc/client": "^11.4.3", + "@trpc/server": "^11.4.3", + "express": "^4.21.2", + "postcss": "^8.5.1", + "solid-js": "^1.9.10", + "redaxios": "^0.5.1", + "tailwindcss": "^4.1.15", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/express": "^4.17.23", + "vite-plugin-solid": "^2.11.10", + "tsx": "^4.20.3", + "vite": "^7.1.7" + } +} diff --git a/examples/solid/with-trpc/postcss.config.mjs b/examples/solid/with-trpc/postcss.config.mjs new file mode 100644 index 00000000000..a7f73a2d1d7 --- /dev/null +++ b/examples/solid/with-trpc/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +} diff --git a/examples/solid/with-trpc/src/main.tsx b/examples/solid/with-trpc/src/main.tsx new file mode 100644 index 00000000000..d99d3054d70 --- /dev/null +++ b/examples/solid/with-trpc/src/main.tsx @@ -0,0 +1,37 @@ +import { render } from 'solid-js/web' +import { RouterProvider, createRouter } from '@tanstack/solid-router' +import { trpc } from './trpc' + +import { Spinner } from './routes/-components/spinner' +import './styles.css' + +// Import the generated route tree +import { routeTree } from './routeTree.gen' + +// Create a router instance +const router = createRouter({ + routeTree, + scrollRestoration: true, + defaultPreload: 'intent', + defaultPendingComponent: () => ( +
+ +
+ ), + context: { + trpc, + }, +}) + +// Register the router instance for type safety +declare module '@tanstack/solid-router' { + interface Register { + router: typeof router + } +} + +const rootElement = document.getElementById('root')! + +if (!rootElement.innerHTML) { + render(() => , rootElement) +} diff --git a/examples/solid/with-trpc/src/routeTree.gen.ts b/examples/solid/with-trpc/src/routeTree.gen.ts new file mode 100644 index 00000000000..fd935076940 --- /dev/null +++ b/examples/solid/with-trpc/src/routeTree.gen.ts @@ -0,0 +1,180 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as DashboardRouteImport } from './routes/dashboard' +import { Route as IndexRouteImport } from './routes/index' +import { Route as DashboardIndexRouteImport } from './routes/dashboard.index' +import { Route as DashboardPostsRouteImport } from './routes/dashboard.posts' +import { Route as DashboardPostsIndexRouteImport } from './routes/dashboard.posts.index' +import { Route as DashboardPostsPostIdRouteImport } from './routes/dashboard.posts.$postId' + +const DashboardRoute = DashboardRouteImport.update({ + id: '/dashboard', + path: '/dashboard', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const DashboardIndexRoute = DashboardIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => DashboardRoute, +} as any) +const DashboardPostsRoute = DashboardPostsRouteImport.update({ + id: '/posts', + path: '/posts', + getParentRoute: () => DashboardRoute, +} as any) +const DashboardPostsIndexRoute = DashboardPostsIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => DashboardPostsRoute, +} as any) +const DashboardPostsPostIdRoute = DashboardPostsPostIdRouteImport.update({ + id: '/$postId', + path: '/$postId', + getParentRoute: () => DashboardPostsRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/dashboard': typeof DashboardRouteWithChildren + '/dashboard/posts': typeof DashboardPostsRouteWithChildren + '/dashboard/': typeof DashboardIndexRoute + '/dashboard/posts/$postId': typeof DashboardPostsPostIdRoute + '/dashboard/posts/': typeof DashboardPostsIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/dashboard': typeof DashboardIndexRoute + '/dashboard/posts/$postId': typeof DashboardPostsPostIdRoute + '/dashboard/posts': typeof DashboardPostsIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/dashboard': typeof DashboardRouteWithChildren + '/dashboard/posts': typeof DashboardPostsRouteWithChildren + '/dashboard/': typeof DashboardIndexRoute + '/dashboard/posts/$postId': typeof DashboardPostsPostIdRoute + '/dashboard/posts/': typeof DashboardPostsIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/dashboard' + | '/dashboard/posts' + | '/dashboard/' + | '/dashboard/posts/$postId' + | '/dashboard/posts/' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/dashboard' | '/dashboard/posts/$postId' | '/dashboard/posts' + id: + | '__root__' + | '/' + | '/dashboard' + | '/dashboard/posts' + | '/dashboard/' + | '/dashboard/posts/$postId' + | '/dashboard/posts/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DashboardRoute: typeof DashboardRouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/dashboard': { + id: '/dashboard' + path: '/dashboard' + fullPath: '/dashboard' + preLoaderRoute: typeof DashboardRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/dashboard/': { + id: '/dashboard/' + path: '/' + fullPath: '/dashboard/' + preLoaderRoute: typeof DashboardIndexRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/posts': { + id: '/dashboard/posts' + path: '/posts' + fullPath: '/dashboard/posts' + preLoaderRoute: typeof DashboardPostsRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/posts/': { + id: '/dashboard/posts/' + path: '/' + fullPath: '/dashboard/posts/' + preLoaderRoute: typeof DashboardPostsIndexRouteImport + parentRoute: typeof DashboardPostsRoute + } + '/dashboard/posts/$postId': { + id: '/dashboard/posts/$postId' + path: '/$postId' + fullPath: '/dashboard/posts/$postId' + preLoaderRoute: typeof DashboardPostsPostIdRouteImport + parentRoute: typeof DashboardPostsRoute + } + } +} + +interface DashboardPostsRouteChildren { + DashboardPostsPostIdRoute: typeof DashboardPostsPostIdRoute + DashboardPostsIndexRoute: typeof DashboardPostsIndexRoute +} + +const DashboardPostsRouteChildren: DashboardPostsRouteChildren = { + DashboardPostsPostIdRoute: DashboardPostsPostIdRoute, + DashboardPostsIndexRoute: DashboardPostsIndexRoute, +} + +const DashboardPostsRouteWithChildren = DashboardPostsRoute._addFileChildren( + DashboardPostsRouteChildren, +) + +interface DashboardRouteChildren { + DashboardPostsRoute: typeof DashboardPostsRouteWithChildren + DashboardIndexRoute: typeof DashboardIndexRoute +} + +const DashboardRouteChildren: DashboardRouteChildren = { + DashboardPostsRoute: DashboardPostsRouteWithChildren, + DashboardIndexRoute: DashboardIndexRoute, +} + +const DashboardRouteWithChildren = DashboardRoute._addFileChildren( + DashboardRouteChildren, +) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DashboardRoute: DashboardRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/examples/solid/with-trpc/src/routes/-components/spinner.tsx b/examples/solid/with-trpc/src/routes/-components/spinner.tsx new file mode 100644 index 00000000000..e7bc03dbdbe --- /dev/null +++ b/examples/solid/with-trpc/src/routes/-components/spinner.tsx @@ -0,0 +1,17 @@ +export function Spinner(props: { show?: boolean; wait?: `delay-${number}` }) { + return ( +
+ ⍥ +
+ ) +} diff --git a/examples/solid/with-trpc/src/routes/__root.tsx b/examples/solid/with-trpc/src/routes/__root.tsx new file mode 100644 index 00000000000..d6c8e7bb106 --- /dev/null +++ b/examples/solid/with-trpc/src/routes/__root.tsx @@ -0,0 +1,76 @@ +import { + Link, + Outlet, + createRootRouteWithContext, + useRouterState, +} from '@tanstack/solid-router' +import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools' + +import { Spinner } from './-components/spinner' +import type { trpc } from '../trpc' + +export interface RouterAppContext { + trpc: typeof trpc +} + +export const Route = createRootRouteWithContext()({ + component: RootComponent, +}) + +function RootComponent() { + const isFetching = useRouterState({ select: (s) => s.isLoading }) + + return ( + <> +
+
+

With tRPC

+ {/* Show a global spinner when the router is transitioning */} +
+ +
+
+
+
+ {( + [ + ['/', 'Home'], + ['/dashboard', 'Dashboard'], + ] as const + ).map(([to, label]) => { + return ( +
+ + {label} + +
+ ) + })} +
+
+ {/* Render our first route match */} + +
+
+
+ + + ) +} diff --git a/examples/solid/with-trpc/src/routes/dashboard.index.tsx b/examples/solid/with-trpc/src/routes/dashboard.index.tsx new file mode 100644 index 00000000000..8c2aa43e7d2 --- /dev/null +++ b/examples/solid/with-trpc/src/routes/dashboard.index.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/dashboard/')({ + loader: ({ context: { trpc } }) => trpc.posts.query(), + component: DashboardIndexComponent, +}) + +function DashboardIndexComponent() { + const posts = Route.useLoaderData() + + return ( +
+
+ Welcome to the dashboard! You have{' '} + {posts().length} total posts. +
+
+ ) +} diff --git a/examples/solid/with-trpc/src/routes/dashboard.posts.$postId.tsx b/examples/solid/with-trpc/src/routes/dashboard.posts.$postId.tsx new file mode 100644 index 00000000000..cc967b0b76a --- /dev/null +++ b/examples/solid/with-trpc/src/routes/dashboard.posts.$postId.tsx @@ -0,0 +1,90 @@ +import * as Solid from 'solid-js' +import { Link, createFileRoute } from '@tanstack/solid-router' +import { z } from 'zod' + +export const Route = createFileRoute('/dashboard/posts/$postId')({ + validateSearch: z.object({ + showNotes: z.boolean().optional(), + notes: z.string().optional(), + }), + loader: async ({ + context: { trpc }, + params: { postId }, + }): Promise<{ id: string; title: string } | undefined> => + trpc.post.query(postId), + component: DashboardPostsPostIdComponent, +}) + +function DashboardPostsPostIdComponent() { + const post = Route.useLoaderData() + const search = Route.useSearch() + const navigate = Route.useNavigate() + + const [notes, setNotes] = Solid.createSignal(search().notes ?? ``) + + Solid.createEffect( + Solid.on(notes, () => { + navigate({ + search: (old) => ({ ...old, notes: notes() ? notes() : undefined }), + replace: true, + params: true, + }) + }), + ) + + if (!post()) { + return
Post not found
+ } + + return ( +
+
+

+ +

+
+