-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
docs(solid-router): monorepo-solid-query example #5850
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
51052b1
fe5ea9e
887e120
2f5656a
6551b2b
cf45528
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,12 @@ | ||||
| node_modules | ||||
| .DS_Store | ||||
| dist | ||||
| dist-ssr | ||||
| *.local | ||||
| *.js | ||||
|
|
||||
| /test-results/ | ||||
| /playwright-report/ | ||||
| /blob-report/ | ||||
| /playwright/.cache/ | ||||
| pnpm-workspace.yaml | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove The workspace configuration file must be committed to the repository so that developers and CI systems can work with the monorepo structure. This should not be ignored. Apply this diff: - pnpm-workspace.yaml📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "startCommand": "cp pnpm-workspace.yaml.example pnpm-workspace.yaml && pnpm install && pnpm dev" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "files.watcherExclude": { | ||
| "**/routeTree.gen.ts": true | ||
| }, | ||
| "search.exclude": { | ||
| "**/routeTree.gen.ts": true | ||
| }, | ||
| "files.readonlyInclude": { | ||
| "**/routeTree.gen.ts": true | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # Example of a monorepo with router and feature libraries | ||
|
|
||
| To run this example: | ||
|
|
||
| - `npm install` or `yarn` | ||
| - `npm dev` or `yarn dev` | ||
|
|
||
| A challenge with TanStack Router in a monorepo setup is that it requires TypeScript type augmentations. However, if you set this up directly in the final app, the links inside the libraries won’t be type-safe. To solve this in a monorepo, you need a separate library just for the router, without any components, and then integrate it with the app. | ||
|
|
||
| This example showcases this approach using the following packages: | ||
|
|
||
| - `packages/router` is the router library | ||
| - `packages/post-query` is the post query collection library | ||
| - `packages/post-feature` is the posts ui library | ||
| - `packages/app` is the app | ||
|
|
||
| With this approach, we can use the query options from the data library both in the router and the feature library without creating circular dependencies. | ||
|
|
||
| Since the router library re-exports the router components, importing them in the feature library ensures they remain type-safe, as they’re linked to the TypeScript augmentations. | ||
|
|
||
| Finally, in the app, we can create a map of routes to components ([`packages/app/src/main.tsx`](./packages/app/src/main.tsx)), which ties the router to the components. **We could enforce lazy loading here, but it was left out for simplicity.** With this setup, we now have a fully type-safe router! | ||
|
|
||
| Here is what it looks like in the monorepo: | ||
|
|
||
|  | ||
|
|
||
| ## Stackblitz limitation | ||
|
|
||
| ### TypeScript IDE feedback | ||
|
|
||
| Due to a limitation on Stackblitz, the example's types are not properly inferred in the IDE, however as soon as you click on fork in the bottom right corner, the types should be correctly inferred. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| { | ||
| "name": "router-solid-monorepo-solid-query", | ||
| "version": "1.0.0", | ||
| "scripts": { | ||
| "test": "echo \"Error: no test specified\" && exit 1", | ||
| "post-query": "pnpm --filter @router-solid-mono-solid-query/post-query", | ||
| "post-feature": "pnpm --filter @router-solid-mono-solid-query/post-feature", | ||
| "router": "pnpm --filter @router-solid-mono-solid-query/router", | ||
| "app": "pnpm --filter @router-solid-mono-solid-query/app", | ||
| "dev": "pnpm post-query build && pnpm router build && pnpm post-feature build && pnpm app dev" | ||
| }, | ||
| "dependencies": { | ||
| "@tanstack/solid-query": "^5.90.0", | ||
| "@tanstack/solid-query-devtools": "^5.90.0", | ||
| "@tanstack/solid-router": "^1.135.2", | ||
| "@tanstack/solid-router-devtools": "^1.135.2", | ||
| "@tanstack/router-plugin": "^1.135.2", | ||
| "solid-js": "^1.9.10", | ||
| "redaxios": "^0.5.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^22.10.2", | ||
| "typescript": "^5.7.2", | ||
| "vite-plugin-solid": "^2.11.10", | ||
| "vite": "^7.1.7", | ||
| "vite-plugin-dts": "^4.5.4" | ||
| }, | ||
| "keywords": [], | ||
| "author": "", | ||
| "license": "ISC" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Vite App</title> | ||
| </head> | ||
| <body> | ||
| <div id="app"></div> | ||
| <script type="module" src="/src/main.tsx"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| { | ||
| "name": "@router-solid-mono-solid-query/app", | ||
| "private": true, | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "vite --port=3001", | ||
| "build": "vite build && tsc --noEmit", | ||
| "serve": "vite preview", | ||
| "start": "vite" | ||
| }, | ||
| "dependencies": { | ||
| "@tanstack/solid-query": "^5.90.0", | ||
| "@router-solid-mono-solid-query/post-feature": "workspace:*", | ||
| "@router-solid-mono-solid-query/router": "workspace:*", | ||
| "solid-js": "^1.9.10" | ||
| }, | ||
| "devDependencies": { | ||
| "vite-plugin-solid": "^2.11.10", | ||
| "typescript": "^5.7.2", | ||
| "@tanstack/solid-router-devtools": "^1.135.2", | ||
| "postcss": "^8.5.1", | ||
| "autoprefixer": "^10.4.20", | ||
| "tailwindcss": "^3.4.17", | ||
| "vite": "^7.1.7", | ||
| "vite-plugin-dts": "^4.5.4" | ||
| }, | ||
| "nx": { | ||
| "targets": { | ||
| "dev": { | ||
| "dependsOn": [ | ||
| "^build" | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,60 @@ | ||||||||||||||
| import { render } from 'solid-js/web' | ||||||||||||||
| import { RouterProvider } from '@tanstack/solid-router' | ||||||||||||||
| import { QueryClientProvider } from '@tanstack/solid-query' | ||||||||||||||
| import { queryClient, router } from '@router-solid-mono-solid-query/router' | ||||||||||||||
| import { | ||||||||||||||
| PostErrorComponent, | ||||||||||||||
| PostIdComponent, | ||||||||||||||
| PostsListComponent, | ||||||||||||||
| } from '@router-solid-mono-solid-query/post-feature' | ||||||||||||||
| import { RootComponent } from './rootComponent' | ||||||||||||||
| import type { RouterIds } from '@router-solid-mono-solid-query/router' | ||||||||||||||
| import './style.css' | ||||||||||||||
| import type { JSX } from 'solid-js' | ||||||||||||||
| // Not lazy loaded for simplicity, but you could expose from your library component | ||||||||||||||
| // individually, and enforce here to use react lazy components via typings | ||||||||||||||
| // so that you have code splitting | ||||||||||||||
|
Comment on lines
+14
to
+16
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update comment to reference Solid instead of React. The comment mentions "react lazy components," but this is a Solid example. Apply this diff: -// Not lazy loaded for simplicity, but you could expose from your library component
-// individually, and enforce here to use react lazy components via typings
-// so that you have code splitting
+// Not lazy loaded for simplicity, but you could expose components from your library
+// individually, and enforce here to use Solid's lazy() components via typings
+// for code splitting📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| const routerMap = { | ||||||||||||||
| '/': PostsListComponent, | ||||||||||||||
| '/$postId': PostIdComponent, | ||||||||||||||
| __root__: RootComponent, | ||||||||||||||
| } as const satisfies Record<RouterIds, () => JSX.Element> | ||||||||||||||
|
|
||||||||||||||
| Object.entries(routerMap).forEach(([path, component]) => { | ||||||||||||||
| const foundRoute = router.routesById[path as RouterIds] | ||||||||||||||
|
|
||||||||||||||
| foundRoute.update({ | ||||||||||||||
| component: component, | ||||||||||||||
| }) | ||||||||||||||
| }) | ||||||||||||||
|
|
||||||||||||||
| // And you can do the same logic with custom error pages, and any other properties | ||||||||||||||
| const errorComponentMap = { | ||||||||||||||
| '/': null, | ||||||||||||||
| '/$postId': PostErrorComponent, | ||||||||||||||
| __root__: null, | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| Object.entries(errorComponentMap).forEach(([path, component]) => { | ||||||||||||||
| if (!component) { | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const foundRoute = router.routesById[path as RouterIds] | ||||||||||||||
| foundRoute.update({ | ||||||||||||||
| errorComponent: component, | ||||||||||||||
| }) | ||||||||||||||
| }) | ||||||||||||||
|
|
||||||||||||||
| const rootElement = document.getElementById('app')! | ||||||||||||||
|
|
||||||||||||||
| if (!rootElement.innerHTML) { | ||||||||||||||
| render( | ||||||||||||||
| () => ( | ||||||||||||||
| <QueryClientProvider client={queryClient}> | ||||||||||||||
| <RouterProvider router={router} /> | ||||||||||||||
| </QueryClientProvider> | ||||||||||||||
| ), | ||||||||||||||
| rootElement, | ||||||||||||||
| ) | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { Link, Outlet } from '@router-solid-mono-solid-query/router' | ||
| import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools' | ||
| import { SolidQueryDevtools } from '@tanstack/solid-query-devtools' | ||
|
|
||
| export function RootComponent() { | ||
| return ( | ||
| <> | ||
| <div class="p-2 flex gap-2 text-lg"> | ||
| <Link | ||
| to="/" | ||
| activeProps={{ | ||
| class: 'font-bold', | ||
| }} | ||
| activeOptions={{ exact: true }} | ||
| > | ||
| Posts list | ||
| </Link> | ||
| </div> | ||
| <hr /> | ||
| <Outlet /> | ||
| <SolidQueryDevtools buttonPosition="top-right" /> | ||
| <TanStackRouterDevtools position="bottom-right" /> | ||
| </> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| @tailwind base; | ||
| @tailwind components; | ||
| @tailwind utilities; | ||
|
|
||
| html { | ||
| color-scheme: light dark; | ||
| } | ||
| * { | ||
| @apply border-gray-200 dark:border-gray-800; | ||
| } | ||
| body { | ||
| @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "strict": true, | ||
| "esModuleInterop": true, | ||
| "jsx": "preserve", | ||
| "jsxImportSource": "solid-js", | ||
| "target": "ESNext", | ||
| "moduleResolution": "Bundler", | ||
| "module": "ESNext", | ||
| "lib": ["DOM", "DOM.Iterable", "ES2022"], | ||
| "skipLibCheck": true | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { fileURLToPath } from 'node:url' | ||
| import * as path from 'node:path' | ||
| import { defineConfig } from 'vite' | ||
| import dts from 'vite-plugin-dts' | ||
| import solid from 'vite-plugin-solid' | ||
|
|
||
| const __filename = fileURLToPath(import.meta.url) | ||
| const __dirname = path.dirname(__filename) | ||
|
|
||
| export default defineConfig({ | ||
| root: __dirname, | ||
| cacheDir: '../../node_modules/.vite/packages/router', | ||
|
|
||
| server: { | ||
| port: 4200, | ||
| host: 'localhost', | ||
| }, | ||
|
|
||
| preview: { | ||
| port: 4300, | ||
| host: 'localhost', | ||
| }, | ||
| plugins: [ | ||
| dts({ | ||
| entryRoot: 'src', | ||
| tsconfigPath: path.join(__dirname, 'tsconfig.json'), | ||
| }), | ||
| solid(), | ||
| ], | ||
| build: { | ||
| outDir: './dist', | ||
| emptyOutDir: true, | ||
| commonjsOptions: { | ||
| transformMixedEsModules: true, | ||
| }, | ||
| }, | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| { | ||
| "name": "@router-solid-mono-solid-query/post-feature", | ||
| "private": true, | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "vite build && tsc --noEmit" | ||
| }, | ||
| "main": "./dist/index.js", | ||
| "types": "./dist/index.d.ts", | ||
| "dependencies": { | ||
| "@tanstack/solid-query": "^5.90.0", | ||
| "@router-solid-mono-solid-query/post-query": "workspace:*", | ||
| "@router-solid-mono-solid-query/router": "workspace:*", | ||
| "solid-js": "^1.9.10" | ||
| }, | ||
| "devDependencies": { | ||
| "vite-plugin-solid": "^2.11.10", | ||
| "typescript": "^5.7.2", | ||
| "vite": "^7.1.7", | ||
| "vite-plugin-dts": "^4.5.4" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,28 @@ | ||||||||||
| // import { createEffect } from 'solid-js' | ||||||||||
| import { PostNotFoundError } from '@router-solid-mono-solid-query/post-query' | ||||||||||
| // import { useQueryErrorResetBoundary } from '@tanstack/solid-query' | ||||||||||
|
Comment on lines
+1
to
+3
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove commented-out imports. Dead code should be removed to keep the codebase clean. Apply this diff: -// import { createEffect } from 'solid-js'
import { PostNotFoundError } from '@router-solid-mono-solid-query/post-query'
-// import { useQueryErrorResetBoundary } from '@tanstack/solid-query'
import {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| import { | ||||||||||
| ErrorComponent, | ||||||||||
| useRouter, | ||||||||||
| } from '@router-solid-mono-solid-query/router' | ||||||||||
| import type { ErrorComponentProps } from '@router-solid-mono-solid-query/router' | ||||||||||
|
|
||||||||||
| export function PostErrorComponent({ error, reset }: ErrorComponentProps) { | ||||||||||
| const router = useRouter() | ||||||||||
| if (error instanceof PostNotFoundError) { | ||||||||||
| return <div>{error.message}</div> | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return ( | ||||||||||
| <div> | ||||||||||
| <button | ||||||||||
| onClick={() => { | ||||||||||
| router.invalidate() | ||||||||||
| }} | ||||||||||
| > | ||||||||||
| retry | ||||||||||
| </button> | ||||||||||
| <ErrorComponent error={error} /> | ||||||||||
| </div> | ||||||||||
| ) | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { useQuery } from '@tanstack/solid-query' | ||
| import { postQueryOptions } from '@router-solid-mono-solid-query/post-query' | ||
|
|
||
| import { getRouteApi } from '@router-solid-mono-solid-query/router' | ||
|
|
||
| const route = getRouteApi('/$postId') | ||
|
|
||
| export function PostIdComponent() { | ||
| const postId = route.useParams()().postId | ||
| const { data: post } = useQuery(() => postQueryOptions(postId)) | ||
|
|
||
| return ( | ||
| <div class="space-y-2"> | ||
| <h4 class="text-xl font-bold underline">{post?.title}</h4> | ||
| <div class="text-sm">{post?.body}</div> | ||
| </div> | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Avoid overly broad
*.jspattern.Ignoring all
.jsfiles at the top level could unintentionally exclude source code or configuration files. Narrow this to specific directories (e.g.,/dist/*.jsor/packages/*/dist/) where compiled output lives, or remove it if the monorepo is TypeScript-only.If this is a pure TypeScript monorepo with no committed
.jssource files, consider removing this line entirely. Otherwise, scope it to build output directories.🏁 Script executed:
Length of output: 1304
🏁 Script executed:
Length of output: 1156
Remove
pnpm-workspace.yamlfrom .gitignore (critical for monorepo).Workspace configuration files must be committed to version control so all developers and CI systems can access the monorepo structure. Line 12 should be removed entirely.
Additionally, line 6 (
*.js) is acceptable for a TypeScript-only project but largely redundant sincedistanddist-ssrare already explicitly ignored. Consider removing it unless you're also ignoring .js files in the root directory intentionally.🤖 Prompt for AI Agents