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
feat: expose querykey getter and fix some querykey stuff #3302
Changes from 29 commits
5392463
8638767
bc702cc
5b55386
2711978
30dcc40
d77f3c8
36ffd96
0a97a6b
217db37
964da9c
90a9173
dc92dcf
5813e19
366107d
c1d4497
c909673
4efa3a8
778859d
7770be8
e9f03e1
cca2d30
8b937df
c9b59bd
44179fd
2d25187
d06701a
4b4e12f
377e271
c8b8ee8
cdc5808
dd8062e
04d5333
c92eb39
c1b162e
c8e3ed7
0e519e2
60afce2
72b5ab6
6f195da
d60dea3
f566339
5adc7ec
957a78c
f025343
3ceba17
05f5c8d
88b8052
943fdeb
bb26bc1
1396478
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,13 @@ | ||
# A minimal React tRPC example using @tanstack/react-router | ||
|
||
Requires node 18 (for global fetch). | ||
|
||
## Playing around | ||
|
||
```bash | ||
npm i | ||
npm run dev | ||
``` | ||
|
||
Try editing the ts files to see the type checking in action :) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<script src="https://cdn.tailwindcss.com"></script> | ||
<title>tRPC + @tanstack/router</title> | ||
</head> | ||
<body> | ||
<div id="root"></div> | ||
<script type="module" src="/src/main.tsx"></script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"name": "@examples/tanstack-router-client", | ||
"private": true, | ||
"version": "10.4.3", | ||
"type": "module", | ||
"scripts": { | ||
"dev": "vite" | ||
}, | ||
"dependencies": { | ||
"@tanstack/react-query": "^4.3.8", | ||
"@tanstack/react-router": "0.0.1-beta.29", | ||
"@trpc/client": "^10.4.3", | ||
"@trpc/react-query": "^10.4.3", | ||
"@trpc/server": "^10.4.3", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0" | ||
}, | ||
"devDependencies": { | ||
"@types/react": "^18.0.9", | ||
"@types/react-dom": "^18.0.5", | ||
"@vitejs/plugin-react": "^2.1.0", | ||
"typescript": "^4.8.3", | ||
"vite": "^3.1.3" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { RouterProvider } from '@tanstack/react-router'; | ||
import React from 'react'; | ||
import ReactDOM from 'react-dom/client'; | ||
import { router } from './utils/router'; | ||
import { TRPCProvider } from './utils/trpc'; | ||
|
||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( | ||
<React.StrictMode> | ||
<TRPCProvider> | ||
<RouterProvider router={router} /> | ||
</TRPCProvider> | ||
</React.StrictMode>, | ||
); |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,145 @@ | ||||||||||||
/* eslint-disable react-hooks/rules-of-hooks */ | ||||||||||||
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.
Suggested change
|
||||||||||||
import { | ||||||||||||
Link, | ||||||||||||
Outlet, | ||||||||||||
createReactRouter, | ||||||||||||
createRouteConfig, | ||||||||||||
useMatch, | ||||||||||||
} from '@tanstack/react-router'; | ||||||||||||
import { getPostById, getPosts } from '../../../server/fetchers'; | ||||||||||||
juliusmarminge marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
import { queryClient, trpc } from './trpc'; | ||||||||||||
|
||||||||||||
const rootRoute = createRouteConfig({ | ||||||||||||
component: () => { | ||||||||||||
return ( | ||||||||||||
<> | ||||||||||||
<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> | ||||||||||||
</div> | ||||||||||||
<hr /> | ||||||||||||
<Outlet /> {/* Start rendering router matches */} | ||||||||||||
</> | ||||||||||||
); | ||||||||||||
}, | ||||||||||||
}); | ||||||||||||
|
||||||||||||
const indexRoute = rootRoute.createRoute({ | ||||||||||||
path: '/', | ||||||||||||
component: () => { | ||||||||||||
const hello = trpc.hello.useQuery(); | ||||||||||||
if (!hello.data) return <p>Loading...</p>; | ||||||||||||
return <div>{hello.data}</div>; | ||||||||||||
}, | ||||||||||||
}); | ||||||||||||
|
||||||||||||
const postsRoute = rootRoute.createRoute({ | ||||||||||||
path: 'posts', | ||||||||||||
loaderMaxAge: 0, | ||||||||||||
errorComponent: () => 'Oh crap!', | ||||||||||||
loader: async () => { | ||||||||||||
const postKey = trpc.post.all.getQueryKey(undefined, 'query'); | ||||||||||||
|
||||||||||||
queryClient.getQueryData(postKey) ?? | ||||||||||||
(await queryClient.prefetchQuery(postKey, getPosts)); | ||||||||||||
return {}; | ||||||||||||
}, | ||||||||||||
|
||||||||||||
component: () => { | ||||||||||||
const postsQuery = trpc.post.all.useQuery(); | ||||||||||||
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. this is how i thought it'd be useful |
||||||||||||
|
||||||||||||
return ( | ||||||||||||
<div className="p-2 flex gap-2"> | ||||||||||||
<ul className="list-disc pl-4"> | ||||||||||||
{postsQuery.data?.map((post) => { | ||||||||||||
return ( | ||||||||||||
<li key={post.id} className="whitespace-nowrap"> | ||||||||||||
<Link | ||||||||||||
to={postRoute.id} | ||||||||||||
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> | ||||||||||||
); | ||||||||||||
}, | ||||||||||||
}); | ||||||||||||
|
||||||||||||
const postsIndexRoute = postsRoute.createRoute({ | ||||||||||||
path: '/', | ||||||||||||
|
||||||||||||
component: () => { | ||||||||||||
return ( | ||||||||||||
<> | ||||||||||||
<div>Select a post.</div> | ||||||||||||
</> | ||||||||||||
); | ||||||||||||
}, | ||||||||||||
}); | ||||||||||||
|
||||||||||||
const postRoute = postsRoute.createRoute({ | ||||||||||||
path: '$postId', | ||||||||||||
loader: async ({ params }) => { | ||||||||||||
const postKey = trpc.post.byId.getQueryKey({ id: params.postId }, 'query'); | ||||||||||||
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. Shouldn't we be able to use 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. That doesn't work since this is outside the react tree and we don't have our context stuff here. Might be possible to do something similar to the SSG helpers (or maybe they even work?) 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. Hmmmmmm..... I don't like embedding this example when the DX is not optimal |
||||||||||||
|
||||||||||||
queryClient.getQueryData(postKey) ?? | ||||||||||||
juliusmarminge marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
(await queryClient.prefetchQuery(postKey, () => | ||||||||||||
getPostById(params.postId), | ||||||||||||
)); | ||||||||||||
juliusmarminge marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
|
||||||||||||
return {}; | ||||||||||||
}, | ||||||||||||
|
||||||||||||
component: () => { | ||||||||||||
const { params } = useMatch(postRoute.id); | ||||||||||||
const postQuery = trpc.post.byId.useQuery({ id: params.postId }); | ||||||||||||
|
||||||||||||
return ( | ||||||||||||
<div className="space-y-2"> | ||||||||||||
<h4 className="text-xl font-bold underline">{postQuery.data?.title}</h4> | ||||||||||||
</div> | ||||||||||||
); | ||||||||||||
}, | ||||||||||||
}); | ||||||||||||
|
||||||||||||
const routeConfig = rootRoute.addChildren([ | ||||||||||||
indexRoute, | ||||||||||||
postsRoute.addChildren([postsIndexRoute, postRoute]), | ||||||||||||
]); | ||||||||||||
|
||||||||||||
// Set up a ReactRouter instance | ||||||||||||
export const router = createReactRouter({ | ||||||||||||
routeConfig, | ||||||||||||
defaultPreload: 'intent', | ||||||||||||
}); | ||||||||||||
|
||||||||||||
declare module '@tanstack/react-router' { | ||||||||||||
interface RegisterRouter { | ||||||||||||
router: typeof router; | ||||||||||||
} | ||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; | ||
import { httpBatchLink } from '@trpc/client'; | ||
import { createTRPCReact } from '@trpc/react-query'; | ||
import React from 'react'; | ||
import type { AppRouter } from '../../../server'; | ||
|
||
/** | ||
* Typesafe hooks | ||
*/ | ||
export const trpc = createTRPCReact<AppRouter>(); | ||
|
||
/** | ||
* Create a QueryClient outside of the app, | ||
* so we can use it for route loaders | ||
*/ | ||
export const queryClient = new QueryClient(); | ||
|
||
/** | ||
* A wrapper for your app that provides the TRPC context. | ||
* Use only in _app.tsx | ||
*/ | ||
export const TRPCProvider: React.FC<{ children: React.ReactNode }> = ({ | ||
children, | ||
}) => { | ||
const [trpcClient] = React.useState(() => | ||
trpc.createClient({ | ||
links: [ | ||
httpBatchLink({ | ||
url: 'http://localhost:2023', | ||
}), | ||
], | ||
}), | ||
); | ||
|
||
return ( | ||
<trpc.Provider client={trpcClient} queryClient={queryClient}> | ||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider> | ||
</trpc.Provider> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
{ | ||
"compilerOptions": { | ||
"target": "ESNext", | ||
"useDefineForClassFields": true, | ||
"lib": ["DOM", "DOM.Iterable", "ESNext"], | ||
"allowJs": false, | ||
"skipLibCheck": true, | ||
"esModuleInterop": false, | ||
"allowSyntheticDefaultImports": true, | ||
"strict": true, | ||
"forceConsistentCasingInFileNames": true, | ||
"module": "ESNext", | ||
"moduleResolution": "Node", | ||
"resolveJsonModule": true, | ||
"isolatedModules": true, | ||
"noEmit": true, | ||
"jsx": "react-jsx", | ||
"types": ["vite/client"] | ||
}, | ||
"include": ["src", "vite.config.ts"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import react from '@vitejs/plugin-react'; | ||
import { defineConfig } from 'vite'; | ||
|
||
// https://vitejs.dev/config/ | ||
export default defineConfig({ | ||
server: { | ||
port: 3000, | ||
}, | ||
preview: { | ||
port: 3000, | ||
}, | ||
plugins: [react()], | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"name": "@examples/tanstack-router", | ||
"private": true, | ||
"version": "10.4.3", | ||
"workspaces": [ | ||
"client", | ||
"server" | ||
], | ||
"scripts": { | ||
"dev:client": "npm run dev -w client", | ||
"dev:server": "npm run dev -w server", | ||
"dev": "run-p dev:*" | ||
}, | ||
"devDependencies": { | ||
"npm-run-all": "^4.1.5" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
const db = { | ||
posts: [ | ||
{ id: '1', title: 'hello world' }, | ||
{ id: '2', title: 'foo bar' }, | ||
], | ||
}; | ||
|
||
export const getPosts = async () => { | ||
// simulate slow db | ||
await new Promise((res) => setTimeout(res, 1000)); | ||
return db.posts; | ||
}; | ||
|
||
export const getPostById = async (id: string) => { | ||
// simulate slow db | ||
await new Promise((res) => setTimeout(res, 1000)); | ||
return db.posts.find((post) => post.id === id); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/** | ||
* This is the API-handler of your app that contains all your API routes. | ||
* On a bigger app, you will probably want to split this file up into multiple files. | ||
*/ | ||
import { initTRPC } from '@trpc/server'; | ||
import { createHTTPHandler } from '@trpc/server/adapters/standalone'; | ||
import http from 'http'; | ||
import { z } from 'zod'; | ||
import { getPostById, getPosts } from './fetchers'; | ||
|
||
const t = initTRPC.create(); | ||
|
||
const publicProcedure = t.procedure; | ||
const router = t.router; | ||
|
||
const appRouter = router({ | ||
hello: publicProcedure.query(() => 'Hello world!'), | ||
post: router({ | ||
all: publicProcedure.query(async () => { | ||
return await getPosts(); | ||
}), | ||
byId: publicProcedure | ||
.input(z.object({ id: z.string() })) | ||
.query(async ({ input }) => { | ||
return await getPostById(input.id); | ||
}), | ||
}), | ||
}); | ||
|
||
// export only the type definition of the API | ||
// None of the actual implementation is exposed to the client | ||
export type AppRouter = typeof appRouter; | ||
|
||
// create handler | ||
const handler = createHTTPHandler({ | ||
router: appRouter, | ||
createContext: () => ({}), | ||
}); | ||
|
||
const server = http.createServer((req, res) => { | ||
res.setHeader('Access-Control-Allow-Origin', '*'); | ||
res.setHeader('Access-Control-Request-Method', '*'); | ||
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET'); | ||
res.setHeader('Access-Control-Allow-Headers', '*'); | ||
if (req.method === 'OPTIONS') { | ||
res.writeHead(200); | ||
return res.end(); | ||
} | ||
handler(req, res); | ||
}); | ||
|
||
server.listen(2023); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"name": "@examples/tanstack-router-server", | ||
"version": "10.4.3", | ||
"private": true, | ||
"scripts": { | ||
"dev": "tsx watch index.ts" | ||
}, | ||
"dependencies": { | ||
"@trpc/server": "^10.4.3", | ||
"zod": "^3.0.0" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^18.7.20", | ||
"tsx": "^3.9.0", | ||
"typescript": "^4.8.3" | ||
} | ||
} |
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.
Just a playground for seeing the usage - I don't think we should keep this but instead contribute to https://github.com/TanStack/router/tree/beta/examples/react/with-trpc-react-query
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.
Seems like they haven't solved the data loading either? https://github.com/TanStack/router/blob/484af7fd844c3d4b02d67846ec268bc7310b9aba/examples/react/with-trpc-react-query/client/main.tsx#L111-L114
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.
Maybe just remove the example for now then and revisit later?