@trpc/next 11.14.0 → 11.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/bin/intent.js +20 -0
- package/package.json +19 -9
- package/skills/nextjs-app-router/SKILL.md +429 -0
- package/skills/nextjs-pages-router/SKILL.md +371 -0
package/README.md
CHANGED
|
@@ -36,6 +36,14 @@ pnpm add @trpc/next @trpc/react-query @tanstack/react-query
|
|
|
36
36
|
bun add @trpc/next @trpc/react-query @tanstack/react-query
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## AI Agents
|
|
40
|
+
|
|
41
|
+
If you use an AI coding agent, install tRPC skills for better code generation:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx @tanstack/intent@latest install
|
|
45
|
+
```
|
|
46
|
+
|
|
39
47
|
## Basic Example
|
|
40
48
|
|
|
41
49
|
Setup tRPC in `utils/trpc.ts`.
|
package/bin/intent.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Auto-generated by @tanstack/intent setup
|
|
3
|
+
// Exposes the intent end-user CLI for consumers of this library.
|
|
4
|
+
// Commit this file, then add to your package.json:
|
|
5
|
+
// "bin": { "intent": "./bin/intent.js" }
|
|
6
|
+
try {
|
|
7
|
+
await import('@tanstack/intent/intent-library');
|
|
8
|
+
} catch (e) {
|
|
9
|
+
if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') {
|
|
10
|
+
console.error('@tanstack/intent is not installed.');
|
|
11
|
+
console.error('');
|
|
12
|
+
console.error('Install it as a dev dependency:');
|
|
13
|
+
console.error(' npm add -D @tanstack/intent');
|
|
14
|
+
console.error('');
|
|
15
|
+
console.error('Or run directly:');
|
|
16
|
+
console.error(' npx @tanstack/intent@latest list');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
throw e;
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@trpc/next",
|
|
4
|
-
"version": "11.14.
|
|
4
|
+
"version": "11.14.1",
|
|
5
5
|
"description": "The tRPC Next.js library",
|
|
6
6
|
"author": "KATT",
|
|
7
7
|
"license": "MIT",
|
|
@@ -100,13 +100,16 @@
|
|
|
100
100
|
"app-dir",
|
|
101
101
|
"ssrPrepass",
|
|
102
102
|
"!**/*.test.*",
|
|
103
|
-
"!**/__tests__"
|
|
103
|
+
"!**/__tests__",
|
|
104
|
+
"skills",
|
|
105
|
+
"!skills/_artifacts",
|
|
106
|
+
"bin"
|
|
104
107
|
],
|
|
105
108
|
"peerDependencies": {
|
|
106
109
|
"@tanstack/react-query": "^5.59.15",
|
|
107
|
-
"@trpc/client": "11.14.
|
|
108
|
-
"@trpc/react-query": "11.14.
|
|
109
|
-
"@trpc/server": "11.14.
|
|
110
|
+
"@trpc/client": "11.14.1",
|
|
111
|
+
"@trpc/react-query": "11.14.1",
|
|
112
|
+
"@trpc/server": "11.14.1",
|
|
110
113
|
"next": "*",
|
|
111
114
|
"react": ">=16.8.0",
|
|
112
115
|
"react-dom": ">=16.8.0",
|
|
@@ -121,10 +124,11 @@
|
|
|
121
124
|
}
|
|
122
125
|
},
|
|
123
126
|
"devDependencies": {
|
|
127
|
+
"@tanstack/intent": "^0.0.20",
|
|
124
128
|
"@tanstack/react-query": "^5.80.3",
|
|
125
|
-
"@trpc/client": "11.14.
|
|
126
|
-
"@trpc/react-query": "11.14.
|
|
127
|
-
"@trpc/server": "11.14.
|
|
129
|
+
"@trpc/client": "11.14.1",
|
|
130
|
+
"@trpc/react-query": "11.14.1",
|
|
131
|
+
"@trpc/server": "11.14.1",
|
|
128
132
|
"@types/express": "^5.0.0",
|
|
129
133
|
"@types/node": "^22.13.5",
|
|
130
134
|
"@types/react": "^19.1.0",
|
|
@@ -145,5 +149,11 @@
|
|
|
145
149
|
"funding": [
|
|
146
150
|
"https://trpc.io/sponsor"
|
|
147
151
|
],
|
|
148
|
-
"
|
|
152
|
+
"keywords": [
|
|
153
|
+
"tanstack-intent"
|
|
154
|
+
],
|
|
155
|
+
"bin": {
|
|
156
|
+
"intent": "./bin/intent.js"
|
|
157
|
+
},
|
|
158
|
+
"gitHead": "e896259af491fc4b1c9e8fc320817e2222bae869"
|
|
149
159
|
}
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nextjs-app-router
|
|
3
|
+
description: >
|
|
4
|
+
Full end-to-end tRPC setup for Next.js App Router. Covers route handler
|
|
5
|
+
with fetchRequestHandler (GET + POST exports), TRPCProvider with
|
|
6
|
+
QueryClientProvider, createTRPCOptionsProxy for RSC prefetching,
|
|
7
|
+
HydrateClient/HydrationBoundary for hydration, useSuspenseQuery
|
|
8
|
+
for Suspense, and server-side callers.
|
|
9
|
+
type: framework
|
|
10
|
+
library: trpc
|
|
11
|
+
framework: react
|
|
12
|
+
library_version: '11.14.0'
|
|
13
|
+
requires:
|
|
14
|
+
- server-setup
|
|
15
|
+
- client-setup
|
|
16
|
+
- react-query-setup
|
|
17
|
+
- adapter-fetch
|
|
18
|
+
sources:
|
|
19
|
+
- www/docs/client/nextjs/overview.mdx
|
|
20
|
+
- www/docs/client/tanstack-react-query/server-components.mdx
|
|
21
|
+
- www/docs/server/adapters/nextjs.md
|
|
22
|
+
- examples/next-prisma-starter/
|
|
23
|
+
- examples/next-sse-chat/
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
This skill builds on [server-setup], [client-setup], [react-query-setup], and [adapter-fetch]. Read them first for foundational concepts.
|
|
27
|
+
|
|
28
|
+
# tRPC -- Next.js App Router
|
|
29
|
+
|
|
30
|
+
## File Structure
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
.
|
|
34
|
+
├── app
|
|
35
|
+
│ ├── api/trpc/[trpc]
|
|
36
|
+
│ │ └── route.ts # tRPC HTTP handler
|
|
37
|
+
│ ├── layout.tsx # mount TRPCReactProvider
|
|
38
|
+
│ ├── page.tsx # server component (prefetch)
|
|
39
|
+
│ └── client-greeting.tsx # client component (consume)
|
|
40
|
+
├── trpc
|
|
41
|
+
│ ├── init.ts # initTRPC, createTRPCContext
|
|
42
|
+
│ ├── routers
|
|
43
|
+
│ │ └── _app.ts # main app router, AppRouter type
|
|
44
|
+
│ ├── query-client.ts # shared QueryClient factory
|
|
45
|
+
│ ├── client.tsx # client hooks & TRPCReactProvider
|
|
46
|
+
│ └── server.tsx # server-side proxy & helpers
|
|
47
|
+
└── ...
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Setup
|
|
51
|
+
|
|
52
|
+
### 1. Install
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query zod server-only client-only
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 2. Server init and context
|
|
59
|
+
|
|
60
|
+
```ts title="trpc/init.ts"
|
|
61
|
+
import { initTRPC } from '@trpc/server';
|
|
62
|
+
|
|
63
|
+
export const createTRPCContext = async (opts: { headers: Headers }) => {
|
|
64
|
+
return { userId: 'user_123' };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const t = initTRPC
|
|
68
|
+
.context<Awaited<ReturnType<typeof createTRPCContext>>>()
|
|
69
|
+
.create();
|
|
70
|
+
|
|
71
|
+
export const createTRPCRouter = t.router;
|
|
72
|
+
export const createCallerFactory = t.createCallerFactory;
|
|
73
|
+
export const baseProcedure = t.procedure;
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 3. Define the router
|
|
77
|
+
|
|
78
|
+
```ts title="trpc/routers/_app.ts"
|
|
79
|
+
import { z } from 'zod';
|
|
80
|
+
import { baseProcedure, createTRPCRouter } from '../init';
|
|
81
|
+
|
|
82
|
+
export const appRouter = createTRPCRouter({
|
|
83
|
+
hello: baseProcedure
|
|
84
|
+
.input(z.object({ text: z.string() }))
|
|
85
|
+
.query(({ input }) => ({
|
|
86
|
+
greeting: `hello ${input.text}`,
|
|
87
|
+
})),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export type AppRouter = typeof appRouter;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 4. Route handler (API endpoint)
|
|
94
|
+
|
|
95
|
+
```ts title="app/api/trpc/[trpc]/route.ts"
|
|
96
|
+
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
|
97
|
+
import { createTRPCContext } from '../../../../trpc/init';
|
|
98
|
+
import { appRouter } from '../../../../trpc/routers/_app';
|
|
99
|
+
|
|
100
|
+
const handler = (req: Request) =>
|
|
101
|
+
fetchRequestHandler({
|
|
102
|
+
endpoint: '/api/trpc',
|
|
103
|
+
req,
|
|
104
|
+
router: appRouter,
|
|
105
|
+
createContext: () => createTRPCContext({ headers: req.headers }),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export { handler as GET, handler as POST };
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 5. QueryClient factory
|
|
112
|
+
|
|
113
|
+
```ts title="trpc/query-client.ts"
|
|
114
|
+
import {
|
|
115
|
+
defaultShouldDehydrateQuery,
|
|
116
|
+
QueryClient,
|
|
117
|
+
} from '@tanstack/react-query';
|
|
118
|
+
|
|
119
|
+
export function makeQueryClient() {
|
|
120
|
+
return new QueryClient({
|
|
121
|
+
defaultOptions: {
|
|
122
|
+
queries: {
|
|
123
|
+
staleTime: 30 * 1000,
|
|
124
|
+
},
|
|
125
|
+
dehydrate: {
|
|
126
|
+
shouldDehydrateQuery: (query) =>
|
|
127
|
+
defaultShouldDehydrateQuery(query) ||
|
|
128
|
+
query.state.status === 'pending',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
If using a data transformer (e.g., superjson), add `dehydrate.serializeData` and `hydrate.deserializeData` here.
|
|
136
|
+
|
|
137
|
+
### 6. Client provider (client component)
|
|
138
|
+
|
|
139
|
+
```tsx title="trpc/client.tsx"
|
|
140
|
+
'use client';
|
|
141
|
+
|
|
142
|
+
import type { QueryClient } from '@tanstack/react-query';
|
|
143
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
144
|
+
import { createTRPCClient, httpBatchLink } from '@trpc/client';
|
|
145
|
+
import { createTRPCContext } from '@trpc/tanstack-react-query';
|
|
146
|
+
import { useState } from 'react';
|
|
147
|
+
import { makeQueryClient } from './query-client';
|
|
148
|
+
import type { AppRouter } from './routers/_app';
|
|
149
|
+
|
|
150
|
+
export const { TRPCProvider, useTRPC, useTRPCClient } =
|
|
151
|
+
createTRPCContext<AppRouter>();
|
|
152
|
+
|
|
153
|
+
let browserQueryClient: QueryClient;
|
|
154
|
+
function getQueryClient() {
|
|
155
|
+
if (typeof window === 'undefined') {
|
|
156
|
+
return makeQueryClient();
|
|
157
|
+
}
|
|
158
|
+
if (!browserQueryClient) browserQueryClient = makeQueryClient();
|
|
159
|
+
return browserQueryClient;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getUrl() {
|
|
163
|
+
const base = (() => {
|
|
164
|
+
if (typeof window !== 'undefined') return '';
|
|
165
|
+
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
|
|
166
|
+
return 'http://localhost:3000';
|
|
167
|
+
})();
|
|
168
|
+
return `${base}/api/trpc`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function TRPCReactProvider(props: { children: React.ReactNode }) {
|
|
172
|
+
const queryClient = getQueryClient();
|
|
173
|
+
|
|
174
|
+
const [trpcClient] = useState(() =>
|
|
175
|
+
createTRPCClient<AppRouter>({
|
|
176
|
+
links: [
|
|
177
|
+
httpBatchLink({
|
|
178
|
+
url: getUrl(),
|
|
179
|
+
}),
|
|
180
|
+
],
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<QueryClientProvider client={queryClient}>
|
|
186
|
+
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
|
|
187
|
+
{props.children}
|
|
188
|
+
</TRPCProvider>
|
|
189
|
+
</QueryClientProvider>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 7. Server-side proxy (server component)
|
|
195
|
+
|
|
196
|
+
```tsx title="trpc/server.tsx"
|
|
197
|
+
import 'server-only';
|
|
198
|
+
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
|
|
199
|
+
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
|
|
200
|
+
import type { TRPCQueryOptions } from '@trpc/tanstack-react-query';
|
|
201
|
+
import { headers } from 'next/headers';
|
|
202
|
+
import { cache } from 'react';
|
|
203
|
+
import { createTRPCContext } from './init';
|
|
204
|
+
import { makeQueryClient } from './query-client';
|
|
205
|
+
import { appRouter } from './routers/_app';
|
|
206
|
+
|
|
207
|
+
export const getQueryClient = cache(makeQueryClient);
|
|
208
|
+
|
|
209
|
+
export const trpc = createTRPCOptionsProxy({
|
|
210
|
+
ctx: async () =>
|
|
211
|
+
createTRPCContext({
|
|
212
|
+
headers: await headers(),
|
|
213
|
+
}),
|
|
214
|
+
router: appRouter,
|
|
215
|
+
queryClient: getQueryClient,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
export function HydrateClient(props: { children: React.ReactNode }) {
|
|
219
|
+
const queryClient = getQueryClient();
|
|
220
|
+
return (
|
|
221
|
+
<HydrationBoundary state={dehydrate(queryClient)}>
|
|
222
|
+
{props.children}
|
|
223
|
+
</HydrationBoundary>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(
|
|
228
|
+
queryOptions: T,
|
|
229
|
+
) {
|
|
230
|
+
const queryClient = getQueryClient();
|
|
231
|
+
if (queryOptions.queryKey[1]?.type === 'infinite') {
|
|
232
|
+
void queryClient.prefetchInfiniteQuery(queryOptions as any);
|
|
233
|
+
} else {
|
|
234
|
+
void queryClient.prefetchQuery(queryOptions);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### 8. Mount provider in layout
|
|
240
|
+
|
|
241
|
+
```tsx title="app/layout.tsx"
|
|
242
|
+
import { TRPCReactProvider } from '../trpc/client';
|
|
243
|
+
|
|
244
|
+
export default function RootLayout({
|
|
245
|
+
children,
|
|
246
|
+
}: {
|
|
247
|
+
children: React.ReactNode;
|
|
248
|
+
}) {
|
|
249
|
+
return (
|
|
250
|
+
<html lang="en">
|
|
251
|
+
<body>
|
|
252
|
+
<TRPCReactProvider>{children}</TRPCReactProvider>
|
|
253
|
+
</body>
|
|
254
|
+
</html>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Core Patterns
|
|
260
|
+
|
|
261
|
+
### Prefetch in server component, consume in client component
|
|
262
|
+
|
|
263
|
+
```tsx title="app/page.tsx"
|
|
264
|
+
import { HydrateClient, prefetch, trpc } from '../trpc/server';
|
|
265
|
+
import { ClientGreeting } from './client-greeting';
|
|
266
|
+
|
|
267
|
+
export default async function Home() {
|
|
268
|
+
prefetch(trpc.hello.queryOptions({ text: 'world' }));
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<HydrateClient>
|
|
272
|
+
<ClientGreeting />
|
|
273
|
+
</HydrateClient>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
```tsx title="app/client-greeting.tsx"
|
|
279
|
+
'use client';
|
|
280
|
+
|
|
281
|
+
import { useQuery } from '@tanstack/react-query';
|
|
282
|
+
import { useTRPC } from '../trpc/client';
|
|
283
|
+
|
|
284
|
+
export function ClientGreeting() {
|
|
285
|
+
const trpc = useTRPC();
|
|
286
|
+
const greeting = useQuery(trpc.hello.queryOptions({ text: 'world' }));
|
|
287
|
+
if (!greeting.data) return <div>Loading...</div>;
|
|
288
|
+
return <div>{greeting.data.greeting}</div>;
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Suspense with prefetch
|
|
293
|
+
|
|
294
|
+
```tsx title="app/page.tsx"
|
|
295
|
+
import { Suspense } from 'react';
|
|
296
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
297
|
+
import { HydrateClient, prefetch, trpc } from '../trpc/server';
|
|
298
|
+
import { ClientGreeting } from './client-greeting';
|
|
299
|
+
|
|
300
|
+
export default async function Home() {
|
|
301
|
+
prefetch(trpc.hello.queryOptions({ text: 'world' }));
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<HydrateClient>
|
|
305
|
+
<ErrorBoundary fallback={<div>Something went wrong</div>}>
|
|
306
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
307
|
+
<ClientGreeting />
|
|
308
|
+
</Suspense>
|
|
309
|
+
</ErrorBoundary>
|
|
310
|
+
</HydrateClient>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
```tsx title="app/client-greeting.tsx"
|
|
316
|
+
'use client';
|
|
317
|
+
|
|
318
|
+
import { useSuspenseQuery } from '@tanstack/react-query';
|
|
319
|
+
import { useTRPC } from '../trpc/client';
|
|
320
|
+
|
|
321
|
+
export function ClientGreeting() {
|
|
322
|
+
const trpc = useTRPC();
|
|
323
|
+
const { data } = useSuspenseQuery(trpc.hello.queryOptions({ text: 'world' }));
|
|
324
|
+
return <div>{data.greeting}</div>;
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Direct server caller (data needed on server only)
|
|
329
|
+
|
|
330
|
+
```tsx title="trpc/server.tsx"
|
|
331
|
+
// Add to existing server.tsx
|
|
332
|
+
export const caller = appRouter.createCaller(async () =>
|
|
333
|
+
createTRPCContext({ headers: await headers() }),
|
|
334
|
+
);
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
```tsx title="app/page.tsx"
|
|
338
|
+
import { caller } from '../trpc/server';
|
|
339
|
+
|
|
340
|
+
export default async function Home() {
|
|
341
|
+
const greeting = await caller.hello({ text: 'world' });
|
|
342
|
+
return <div>{greeting.greeting}</div>;
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Note: `caller` results are not stored in the query cache. They cannot hydrate to client components. Use `prefetchQuery` if client components also need the data.
|
|
347
|
+
|
|
348
|
+
### fetchQuery for data on server AND client
|
|
349
|
+
|
|
350
|
+
```tsx title="app/page.tsx"
|
|
351
|
+
import { getQueryClient, HydrateClient, trpc } from '../trpc/server';
|
|
352
|
+
import { ClientGreeting } from './client-greeting';
|
|
353
|
+
|
|
354
|
+
export default async function Home() {
|
|
355
|
+
const queryClient = getQueryClient();
|
|
356
|
+
const greeting = await queryClient.fetchQuery(
|
|
357
|
+
trpc.hello.queryOptions({ text: 'world' }),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Use greeting on the server
|
|
361
|
+
console.log(greeting.greeting);
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<HydrateClient>
|
|
365
|
+
<ClientGreeting />
|
|
366
|
+
</HydrateClient>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## Common Mistakes
|
|
372
|
+
|
|
373
|
+
### Not exporting both GET and POST from route handler
|
|
374
|
+
|
|
375
|
+
Next.js App Router route handlers must export named `GET` and `POST` functions. Missing either causes queries or mutations to return 405 Method Not Allowed.
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
// WRONG
|
|
379
|
+
export default function handler(req: Request) { ... }
|
|
380
|
+
|
|
381
|
+
// CORRECT
|
|
382
|
+
const handler = (req: Request) =>
|
|
383
|
+
fetchRequestHandler({ req, router: appRouter, endpoint: '/api/trpc', createContext });
|
|
384
|
+
export { handler as GET, handler as POST };
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Creating a singleton QueryClient for SSR
|
|
388
|
+
|
|
389
|
+
In server components, each request needs its own `QueryClient` instance. A singleton leaks data between requests.
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
// WRONG
|
|
393
|
+
const queryClient = new QueryClient(); // shared across requests!
|
|
394
|
+
|
|
395
|
+
// CORRECT
|
|
396
|
+
export const getQueryClient = cache(makeQueryClient);
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The `cache()` wrapper from React ensures the same `QueryClient` is reused within a single request but a new one is created for each new request.
|
|
400
|
+
|
|
401
|
+
### Missing dehydrate/shouldDehydrateQuery config
|
|
402
|
+
|
|
403
|
+
RSC hydration requires `shouldDehydrateQuery` to include pending queries so that prefetched-but-not-yet-resolved promises can stream to the client. Without this, prefetched queries may not appear in the hydrated state.
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
// WRONG
|
|
407
|
+
new QueryClient(); // default shouldDehydrateQuery skips pending
|
|
408
|
+
|
|
409
|
+
// CORRECT
|
|
410
|
+
new QueryClient({
|
|
411
|
+
defaultOptions: {
|
|
412
|
+
dehydrate: {
|
|
413
|
+
shouldDehydrateQuery: (query) =>
|
|
414
|
+
defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Suspense query failure crashes entire page during SSR
|
|
421
|
+
|
|
422
|
+
If a query fails during SSR with `useSuspenseQuery`, the entire page crashes. Error Boundaries only catch errors on the client side. For critical pages, either handle errors server-side before rendering, or use `useQuery` (non-suspense) which allows graceful degradation.
|
|
423
|
+
|
|
424
|
+
## See Also
|
|
425
|
+
|
|
426
|
+
- [react-query-setup] -- TanStack React Query setup, queryOptions/mutationOptions factories
|
|
427
|
+
- [adapter-fetch] -- fetchRequestHandler for edge/serverless runtimes
|
|
428
|
+
- [server-setup] -- initTRPC, routers, procedures, context
|
|
429
|
+
- [nextjs-pages-router] -- if maintaining a Pages Router project alongside App Router
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nextjs-pages-router
|
|
3
|
+
description: >
|
|
4
|
+
Set up tRPC in Next.js Pages Router with createNextApiHandler,
|
|
5
|
+
createTRPCNext, withTRPC HOC, SSR via ssr option and ssrPrepass,
|
|
6
|
+
SSG via createServerSideHelpers with getStaticProps, and
|
|
7
|
+
server-side helpers for getServerSideProps prefetching.
|
|
8
|
+
type: framework
|
|
9
|
+
library: trpc
|
|
10
|
+
framework: react
|
|
11
|
+
library_version: '11.14.0'
|
|
12
|
+
requires:
|
|
13
|
+
- server-setup
|
|
14
|
+
- client-setup
|
|
15
|
+
sources:
|
|
16
|
+
- www/docs/client/nextjs/overview.mdx
|
|
17
|
+
- www/docs/server/adapters/nextjs.md
|
|
18
|
+
- examples/next-prisma-starter/
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
This skill builds on [server-setup] and [client-setup]. Read them first for foundational concepts.
|
|
22
|
+
|
|
23
|
+
# tRPC -- Next.js Pages Router
|
|
24
|
+
|
|
25
|
+
## File Structure
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
.
|
|
29
|
+
├── src
|
|
30
|
+
│ ├── pages
|
|
31
|
+
│ │ ├── _app.tsx # withTRPC() HOC
|
|
32
|
+
│ │ ├── api/trpc
|
|
33
|
+
│ │ │ └── [trpc].ts # tRPC API handler
|
|
34
|
+
│ │ └── index.tsx # page using tRPC hooks
|
|
35
|
+
│ ├── server
|
|
36
|
+
│ │ ├── routers
|
|
37
|
+
│ │ │ └── _app.ts # main app router
|
|
38
|
+
│ │ ├── context.ts # createContext
|
|
39
|
+
│ │ └── trpc.ts # initTRPC, procedure helpers
|
|
40
|
+
│ └── utils
|
|
41
|
+
│ └── trpc.ts # createTRPCNext, hooks
|
|
42
|
+
└── ...
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Setup
|
|
46
|
+
|
|
47
|
+
### 1. Install
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. Server init
|
|
54
|
+
|
|
55
|
+
```ts title="server/trpc.ts"
|
|
56
|
+
import { initTRPC } from '@trpc/server';
|
|
57
|
+
|
|
58
|
+
const t = initTRPC.create();
|
|
59
|
+
|
|
60
|
+
export const router = t.router;
|
|
61
|
+
export const procedure = t.procedure;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. Define the router
|
|
65
|
+
|
|
66
|
+
```ts title="server/routers/_app.ts"
|
|
67
|
+
import { z } from 'zod';
|
|
68
|
+
import { procedure, router } from '../trpc';
|
|
69
|
+
|
|
70
|
+
export const appRouter = router({
|
|
71
|
+
hello: procedure.input(z.object({ text: z.string() })).query(({ input }) => ({
|
|
72
|
+
greeting: `hello ${input.text}`,
|
|
73
|
+
})),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export type AppRouter = typeof appRouter;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 4. API handler
|
|
80
|
+
|
|
81
|
+
```ts title="pages/api/trpc/[trpc].ts"
|
|
82
|
+
import { createNextApiHandler } from '@trpc/server/adapters/next';
|
|
83
|
+
import { appRouter } from '../../../server/routers/_app';
|
|
84
|
+
|
|
85
|
+
export default createNextApiHandler({
|
|
86
|
+
router: appRouter,
|
|
87
|
+
createContext: () => ({}),
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 5. Create tRPC hooks
|
|
92
|
+
|
|
93
|
+
```ts title="utils/trpc.ts"
|
|
94
|
+
import { httpBatchLink } from '@trpc/client';
|
|
95
|
+
import { createTRPCNext } from '@trpc/next';
|
|
96
|
+
import type { AppRouter } from '../server/routers/_app';
|
|
97
|
+
|
|
98
|
+
function getBaseUrl() {
|
|
99
|
+
if (typeof window !== 'undefined') return '';
|
|
100
|
+
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
|
|
101
|
+
return `http://localhost:${process.env.PORT ?? 3000}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const trpc = createTRPCNext<AppRouter>({
|
|
105
|
+
config() {
|
|
106
|
+
return {
|
|
107
|
+
links: [
|
|
108
|
+
httpBatchLink({
|
|
109
|
+
url: `${getBaseUrl()}/api/trpc`,
|
|
110
|
+
}),
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
ssr: false,
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 6. Wrap app with withTRPC HOC
|
|
119
|
+
|
|
120
|
+
```tsx title="pages/_app.tsx"
|
|
121
|
+
import type { AppType } from 'next/app';
|
|
122
|
+
import { trpc } from '../utils/trpc';
|
|
123
|
+
|
|
124
|
+
const MyApp: AppType = ({ Component, pageProps }) => {
|
|
125
|
+
return <Component {...pageProps} />;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export default trpc.withTRPC(MyApp);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 7. Use hooks in pages
|
|
132
|
+
|
|
133
|
+
```tsx title="pages/index.tsx"
|
|
134
|
+
import { trpc } from '../utils/trpc';
|
|
135
|
+
|
|
136
|
+
export default function IndexPage() {
|
|
137
|
+
const hello = trpc.hello.useQuery({ text: 'client' });
|
|
138
|
+
if (!hello.data) return <div>Loading...</div>;
|
|
139
|
+
return <p>{hello.data.greeting}</p>;
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Core Patterns
|
|
144
|
+
|
|
145
|
+
### SSR with ssr: true
|
|
146
|
+
|
|
147
|
+
Enable SSR to prefetch all queries on the server automatically. Requires `ssrPrepass` and forwarding client headers.
|
|
148
|
+
|
|
149
|
+
```ts title="utils/trpc.ts"
|
|
150
|
+
import { httpBatchLink } from '@trpc/client';
|
|
151
|
+
import { createTRPCNext } from '@trpc/next';
|
|
152
|
+
import { ssrPrepass } from '@trpc/next/ssrPrepass';
|
|
153
|
+
import type { AppRouter } from '../server/routers/_app';
|
|
154
|
+
|
|
155
|
+
function getBaseUrl() {
|
|
156
|
+
if (typeof window !== 'undefined') return '';
|
|
157
|
+
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
|
|
158
|
+
return `http://localhost:${process.env.PORT ?? 3000}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const trpc = createTRPCNext<AppRouter>({
|
|
162
|
+
ssr: true,
|
|
163
|
+
ssrPrepass,
|
|
164
|
+
config({ ctx }) {
|
|
165
|
+
if (typeof window !== 'undefined') {
|
|
166
|
+
return {
|
|
167
|
+
links: [httpBatchLink({ url: '/api/trpc' })],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
links: [
|
|
172
|
+
httpBatchLink({
|
|
173
|
+
url: `${getBaseUrl()}/api/trpc`,
|
|
174
|
+
headers() {
|
|
175
|
+
if (!ctx?.req?.headers) return {};
|
|
176
|
+
return { cookie: ctx.req.headers.cookie };
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### SSG with createServerSideHelpers and getStaticProps
|
|
186
|
+
|
|
187
|
+
```tsx title="pages/posts/[id].tsx"
|
|
188
|
+
import { createServerSideHelpers } from '@trpc/react-query/server';
|
|
189
|
+
import type {
|
|
190
|
+
GetStaticPaths,
|
|
191
|
+
GetStaticPropsContext,
|
|
192
|
+
InferGetStaticPropsType,
|
|
193
|
+
} from 'next';
|
|
194
|
+
import superjson from 'superjson';
|
|
195
|
+
import { appRouter } from '../../server/routers/_app';
|
|
196
|
+
import { trpc } from '../../utils/trpc';
|
|
197
|
+
|
|
198
|
+
export async function getStaticProps(
|
|
199
|
+
context: GetStaticPropsContext<{ id: string }>,
|
|
200
|
+
) {
|
|
201
|
+
const helpers = createServerSideHelpers({
|
|
202
|
+
router: appRouter,
|
|
203
|
+
ctx: {},
|
|
204
|
+
transformer: superjson,
|
|
205
|
+
});
|
|
206
|
+
const id = context.params?.id as string;
|
|
207
|
+
|
|
208
|
+
await helpers.post.byId.prefetch({ id });
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
props: {
|
|
212
|
+
trpcState: helpers.dehydrate(),
|
|
213
|
+
id,
|
|
214
|
+
},
|
|
215
|
+
revalidate: 1,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export const getStaticPaths: GetStaticPaths = async () => {
|
|
220
|
+
return { paths: [], fallback: 'blocking' };
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export default function PostPage(
|
|
224
|
+
props: InferGetStaticPropsType<typeof getStaticProps>,
|
|
225
|
+
) {
|
|
226
|
+
const { id } = props;
|
|
227
|
+
const postQuery = trpc.post.byId.useQuery({ id });
|
|
228
|
+
|
|
229
|
+
if (postQuery.status !== 'success') return <>Loading...</>;
|
|
230
|
+
return <h1>{postQuery.data.title}</h1>;
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Server-side helpers with getServerSideProps
|
|
235
|
+
|
|
236
|
+
```tsx title="pages/posts/[id].tsx"
|
|
237
|
+
import { createServerSideHelpers } from '@trpc/react-query/server';
|
|
238
|
+
import type {
|
|
239
|
+
GetServerSidePropsContext,
|
|
240
|
+
InferGetServerSidePropsType,
|
|
241
|
+
} from 'next';
|
|
242
|
+
import superjson from 'superjson';
|
|
243
|
+
import { appRouter } from '../../server/routers/_app';
|
|
244
|
+
import { trpc } from '../../utils/trpc';
|
|
245
|
+
|
|
246
|
+
export async function getServerSideProps(
|
|
247
|
+
context: GetServerSidePropsContext<{ id: string }>,
|
|
248
|
+
) {
|
|
249
|
+
const helpers = createServerSideHelpers({
|
|
250
|
+
router: appRouter,
|
|
251
|
+
ctx: {},
|
|
252
|
+
transformer: superjson,
|
|
253
|
+
});
|
|
254
|
+
const id = context.params?.id as string;
|
|
255
|
+
|
|
256
|
+
await helpers.post.byId.prefetch({ id });
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
props: {
|
|
260
|
+
trpcState: helpers.dehydrate(),
|
|
261
|
+
id,
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export default function PostPage(
|
|
267
|
+
props: InferGetServerSidePropsType<typeof getServerSideProps>,
|
|
268
|
+
) {
|
|
269
|
+
const { id } = props;
|
|
270
|
+
const postQuery = trpc.post.byId.useQuery({ id });
|
|
271
|
+
|
|
272
|
+
if (postQuery.status !== 'success') return <>Loading...</>;
|
|
273
|
+
return <h1>{postQuery.data.title}</h1>;
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### SSR response caching
|
|
278
|
+
|
|
279
|
+
```ts title="utils/trpc.ts"
|
|
280
|
+
import { httpBatchLink } from '@trpc/client';
|
|
281
|
+
import { createTRPCNext } from '@trpc/next';
|
|
282
|
+
import { ssrPrepass } from '@trpc/next/ssrPrepass';
|
|
283
|
+
import type { AppRouter } from '../server/routers/_app';
|
|
284
|
+
|
|
285
|
+
export const trpc = createTRPCNext<AppRouter>({
|
|
286
|
+
ssr: true,
|
|
287
|
+
ssrPrepass,
|
|
288
|
+
config() {
|
|
289
|
+
return {
|
|
290
|
+
links: [httpBatchLink({ url: '/api/trpc' })],
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
responseMeta(opts) {
|
|
294
|
+
const { clientErrors } = opts;
|
|
295
|
+
if (clientErrors.length) {
|
|
296
|
+
return { status: clientErrors[0].data?.httpStatus ?? 500 };
|
|
297
|
+
}
|
|
298
|
+
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
|
|
299
|
+
return {
|
|
300
|
+
headers: new Headers([
|
|
301
|
+
[
|
|
302
|
+
'cache-control',
|
|
303
|
+
`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
|
|
304
|
+
],
|
|
305
|
+
]),
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### CORS on the API handler
|
|
312
|
+
|
|
313
|
+
```ts title="pages/api/trpc/[trpc].ts"
|
|
314
|
+
import { createNextApiHandler } from '@trpc/server/adapters/next';
|
|
315
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
316
|
+
import { appRouter } from '../../../server/routers/_app';
|
|
317
|
+
|
|
318
|
+
const nextApiHandler = createNextApiHandler({
|
|
319
|
+
router: appRouter,
|
|
320
|
+
createContext: () => ({}),
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
export default async function handler(
|
|
324
|
+
req: NextApiRequest,
|
|
325
|
+
res: NextApiResponse,
|
|
326
|
+
) {
|
|
327
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
328
|
+
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET, POST');
|
|
329
|
+
res.setHeader('Access-Control-Allow-Headers', '*');
|
|
330
|
+
|
|
331
|
+
if (req.method === 'OPTIONS') {
|
|
332
|
+
res.writeHead(200);
|
|
333
|
+
return res.end();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return nextApiHandler(req, res);
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Common Mistakes
|
|
341
|
+
|
|
342
|
+
### Using ssr: true without understanding implications
|
|
343
|
+
|
|
344
|
+
Enabling `ssr: true` imports `react-dom` and runs `ssrPrepass` on every request, rendering the component tree repeatedly until no queries are fetching. This adds latency and server load. For better control, keep `ssr: false` (the default) and use `createServerSideHelpers` in `getServerSideProps` or `getStaticProps` to selectively prefetch only the queries you need.
|
|
345
|
+
|
|
346
|
+
### SSR prepass renders multiple times
|
|
347
|
+
|
|
348
|
+
The SSR prepass loop re-renders the component tree repeatedly until all queries resolve. This is by design but causes performance issues with expensive renders. Keep SSR-rendered pages lightweight, or switch to selective prefetching with server-side helpers.
|
|
349
|
+
|
|
350
|
+
### Mixing App Router and Pages Router patterns
|
|
351
|
+
|
|
352
|
+
App Router uses `fetchRequestHandler`, `createTRPCOptionsProxy`, and `@trpc/tanstack-react-query`. Pages Router uses `createNextApiHandler`, `createTRPCNext`, and `@trpc/next`/`@trpc/react-query`. Applying App Router patterns (like `HydrationBoundary` or `prefetchQuery`) in Pages Router, or vice versa, produces non-functional code.
|
|
353
|
+
|
|
354
|
+
### Forgetting to return trpcState from getStaticProps/getServerSideProps
|
|
355
|
+
|
|
356
|
+
When using `createServerSideHelpers`, you must return `trpcState: helpers.dehydrate()` in props. Without this, the prefetched data is lost and queries re-fetch on the client.
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
// WRONG
|
|
360
|
+
return { props: { id } }; // missing trpcState!
|
|
361
|
+
|
|
362
|
+
// CORRECT
|
|
363
|
+
return { props: { trpcState: helpers.dehydrate(), id } };
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## See Also
|
|
367
|
+
|
|
368
|
+
- [server-setup] -- initTRPC, routers, procedures, context
|
|
369
|
+
- [client-setup] -- vanilla tRPC client, links configuration
|
|
370
|
+
- [nextjs-app-router] -- if migrating to or starting with App Router
|
|
371
|
+
- [react-query-classic-migration] -- migrating from @trpc/react-query to @trpc/tanstack-react-query
|