Hoppa till huvudinnehållet
Version: 11.x

Konfigurera med Next.js App Router

Inofficiell Beta-översättning

Denna sida har översatts av PageTurner AI (beta). Inte officiellt godkänd av projektet. Hittade du ett fel? Rapportera problem →

notering

Vi rekommenderar att läsa TanStack React Querys dokumentation om Avancerad serverrendering för att förstå olika typer av serverrendering och fallgropar att undvika.

Rekommenderad filstruktur

graphql
.
├── src
│ ├── app
│ │ ├── api
│ │ │ └── trpc
│ │ │ └── [trpc]
│ │ │ └── route.ts # <-- tRPC HTTP handler
│ │ ├── layout.tsx # <-- mount TRPCReactProvider
│ │ └── page.tsx # <-- server component
│ ├── trpc
│ │ ├── init.ts # <-- tRPC server init & context
│ │ ├── routers
│ │ │ ├── _app.ts # <-- main app router
│ │ │ ├── post.ts # <-- sub routers
│ │ │ └── [..]
│ │ ├── client.tsx # <-- client hooks & provider
│ │ ├── query-client.ts # <-- shared QueryClient factory
│ │ └── server.tsx # <-- server-side caller
│ └── [..]
└── [..]
graphql
.
├── src
│ ├── app
│ │ ├── api
│ │ │ └── trpc
│ │ │ └── [trpc]
│ │ │ └── route.ts # <-- tRPC HTTP handler
│ │ ├── layout.tsx # <-- mount TRPCReactProvider
│ │ └── page.tsx # <-- server component
│ ├── trpc
│ │ ├── init.ts # <-- tRPC server init & context
│ │ ├── routers
│ │ │ ├── _app.ts # <-- main app router
│ │ │ ├── post.ts # <-- sub routers
│ │ │ └── [..]
│ │ ├── client.tsx # <-- client hooks & provider
│ │ ├── query-client.ts # <-- shared QueryClient factory
│ │ └── server.tsx # <-- server-side caller
│ └── [..]
└── [..]

Lägg till tRPC i ett befintligt Next.js App Router-projekt

1. Installera beroenden

npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only
AI-agenter

Om du använder en AI-kodningsagent, installera tRPC-färdigheter för bättre kodgenerering:

bash
npx @tanstack/intent@latest install
bash
npx @tanstack/intent@latest install

2. Skapa en tRPC-router

Initiera din tRPC-backend i trpc/init.ts med funktionen initTRPC och skapa din första router. Här skapar vi en enkel "hello world"-router och procedur - för mer djupgående information om att skapa ditt tRPC-API, se Snabbstartsguiden och Dokumentation för backend-användning.

trpc/init.ts
ts
import { initTRPC } from '@trpc/server';
 
/**
* This context creator accepts `headers` so it can be reused in both
* the RSC server caller (where you pass `next/headers`) and the
* API route handler (where you pass the request headers).
*/
export const createTRPCContext = async (opts: { headers: Headers }) => {
// const user = await auth(opts.headers);
return { userId: 'user_123' };
};
 
// Avoid exporting the entire t-object
// since it's not very descriptive.
// For instance, the use of a t variable
// is common in i18n libraries.
const t = initTRPC
.context<Awaited<ReturnType<typeof createTRPCContext>>>()
.create({
/**
* @see https://trpc.io/docs/server/data-transformers
*/
// transformer: superjson,
});
 
// Base router and procedure helpers
export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;
export const baseProcedure = t.procedure;
trpc/init.ts
ts
import { initTRPC } from '@trpc/server';
 
/**
* This context creator accepts `headers` so it can be reused in both
* the RSC server caller (where you pass `next/headers`) and the
* API route handler (where you pass the request headers).
*/
export const createTRPCContext = async (opts: { headers: Headers }) => {
// const user = await auth(opts.headers);
return { userId: 'user_123' };
};
 
// Avoid exporting the entire t-object
// since it's not very descriptive.
// For instance, the use of a t variable
// is common in i18n libraries.
const t = initTRPC
.context<Awaited<ReturnType<typeof createTRPCContext>>>()
.create({
/**
* @see https://trpc.io/docs/server/data-transformers
*/
// transformer: superjson,
});
 
// Base router and procedure helpers
export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;
export const baseProcedure = t.procedure;

trpc/routers/_app.ts
ts
import { z } from 'zod';
import { baseProcedure, createTRPCRouter } from '../init';
 
export const appRouter = createTRPCRouter({
hello: baseProcedure
.input(
z.object({
text: z.string(),
}),
)
.query((opts) => {
return {
greeting: `hello ${opts.input.text}`,
};
}),
});
 
// export type definition of API
export type AppRouter = typeof appRouter;
trpc/routers/_app.ts
ts
import { z } from 'zod';
import { baseProcedure, createTRPCRouter } from '../init';
 
export const appRouter = createTRPCRouter({
hello: baseProcedure
.input(
z.object({
text: z.string(),
}),
)
.query((opts) => {
return {
greeting: `hello ${opts.input.text}`,
};
}),
});
 
// export type definition of API
export type AppRouter = typeof appRouter;

3. Skapa API route handler

Med App Router använder du fetch-adaptern för att hantera tRPC-förfrågningar. Skapa en route handler som exporterar både GET och POST:

app/api/trpc/[trpc]/route.ts
ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { createTRPCContext } from './trpc/init';
import { appRouter } from './trpc/routers/_app';
 
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => createTRPCContext({ headers: req.headers }),
});
 
export { handler as GET, handler as POST };
app/api/trpc/[trpc]/route.ts
ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { createTRPCContext } from './trpc/init';
import { appRouter } from './trpc/routers/_app';
 
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => createTRPCContext({ headers: req.headers }),
});
 
export { handler as GET, handler as POST };
notering

App Router använder fetch-adaptern (via fetchRequestHandler) istället för den Next.js-specifika adapter som används av Pages Router. Detta beror på att App Router route handlers bygger på standardwebbobjekten Request och Response.

4. Skapa en Query Client-fabrik

Skapa en delad fil trpc/query-client.ts som exporterar en funktion som skapar en QueryClient-instans.

trpc/query-client.ts
ts
import {
defaultShouldDehydrateQuery,
QueryClient,
} from '@tanstack/react-query';
import superjson from 'superjson';
 
export function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 30 * 1000,
},
dehydrate: {
// serializeData: superjson.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === 'pending',
},
hydrate: {
// deserializeData: superjson.deserialize,
},
},
});
}
trpc/query-client.ts
ts
import {
defaultShouldDehydrateQuery,
QueryClient,
} from '@tanstack/react-query';
import superjson from 'superjson';
 
export function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 30 * 1000,
},
dehydrate: {
// serializeData: superjson.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === 'pending',
},
hydrate: {
// deserializeData: superjson.deserialize,
},
},
});
}

Vi sätter några standardalternativ här:

  • staleTime: Med SSR vill vi vanligtvis sätta en standard staleTime över 0 för att undvika omedelbar hämtning på klienten.

  • shouldDehydrateQuery: Det här är en funktion som avgör om en fråga ska dehydreras eller inte. Eftersom RSC-transportprotokollet stöder hydrering av löften över nätverket utökar vi funktionen defaultShouldDehydrateQuery till att även inkludera frågor som fortfarande väntar. Detta låter oss börja med förhämtning i en serverkomponent högt upp i trädet och sedan konsumera det löftet i en klientkomponent längre ner.

  • serializeData och deserializeData (valfritt): Om du konfigurerade en datatransformer i föregående steg, sätt det här alternativet för att säkerställa att data serialiseras korrekt när query client hydreras över server-klient-gränsen.

5. Skapa en tRPC-klient för klientkomponenter

trpc/client.tsx är ingångspunkten när du använder ditt tRPC-API från klientkomponenter. Importera här typdefinitionen för din tRPC-router och skapa typsäkra hooks med createTRPCContext. Vi exporterar också vår context-provider från den här filen.

trpc/client.tsx
tsx
'use client';
 
// ^-- to make sure we can mount the Provider from a server component
import type { QueryClient } from '@tanstack/react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import { createTRPCContext } from '@trpc/tanstack-react-query';
import { useState } from 'react';
import { makeQueryClient } from './query-client';
import type { AppRouter } from './routers/_app';
 
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
 
let browserQueryClient: QueryClient;
function getQueryClient() {
if (typeof window === 'undefined') {
// Server: always make a new query client
return makeQueryClient();
}
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
 
function getUrl() {
const base = (() => {
if (typeof window !== 'undefined') return '';
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return 'http://localhost:3000';
})();
return `${base}/api/trpc`;
}
 
export function TRPCReactProvider(
props: Readonly<{
children: React.ReactNode;
}>,
) {
// NOTE: Avoid useState when initializing the query client if you don't
// have a suspense boundary between this and the code that may
// suspend because React will throw away the client on the initial
// render if it suspends and there is no boundary
const queryClient = getQueryClient();
 
const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({
links: [
httpBatchLink({
// transformer: superjson, <-- if you use a data transformer
url: getUrl(),
}),
],
}),
);
 
return (
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
{props.children}
</TRPCProvider>
</QueryClientProvider>
);
}
trpc/client.tsx
tsx
'use client';
 
// ^-- to make sure we can mount the Provider from a server component
import type { QueryClient } from '@tanstack/react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import { createTRPCContext } from '@trpc/tanstack-react-query';
import { useState } from 'react';
import { makeQueryClient } from './query-client';
import type { AppRouter } from './routers/_app';
 
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
 
let browserQueryClient: QueryClient;
function getQueryClient() {
if (typeof window === 'undefined') {
// Server: always make a new query client
return makeQueryClient();
}
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
 
function getUrl() {
const base = (() => {
if (typeof window !== 'undefined') return '';
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return 'http://localhost:3000';
})();
return `${base}/api/trpc`;
}
 
export function TRPCReactProvider(
props: Readonly<{
children: React.ReactNode;
}>,
) {
// NOTE: Avoid useState when initializing the query client if you don't
// have a suspense boundary between this and the code that may
// suspend because React will throw away the client on the initial
// render if it suspends and there is no boundary
const queryClient = getQueryClient();
 
const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({
links: [
httpBatchLink({
// transformer: superjson, <-- if you use a data transformer
url: getUrl(),
}),
],
}),
);
 
return (
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
{props.children}
</TRPCProvider>
</QueryClientProvider>
);
}

Montera providern i root-layouten för din applikation:

app/layout.tsx
tsx
import { TRPCReactProvider } from '~/trpc/client';
 
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<TRPCReactProvider>{children}</TRPCReactProvider>
</body>
</html>
);
}
app/layout.tsx
tsx
import { TRPCReactProvider } from '~/trpc/client';
 
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<TRPCReactProvider>{children}</TRPCReactProvider>
</body>
</html>
);
}

6. Skapa en tRPC-anropare för serverkomponenter

För att förhämta frågor från serverkomponenter skapar vi en proxy från vår router. Du kan också skicka med en klient om din router finns på en separat server.

trpc/server.tsx
tsx
import 'server-only'; // <-- ensure this file cannot be imported from the client
 
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import { headers } from 'next/headers';
import { cache } from 'react';
import { createTRPCContext } from './init';
import { makeQueryClient } from './query-client';
import { appRouter } from './routers/_app';
 
// IMPORTANT: Create a stable getter for the query client that
// will return the same client during the same request.
export const getQueryClient = cache(makeQueryClient);
 
export const trpc = createTRPCOptionsProxy({
ctx: async () =>
createTRPCContext({
headers: await headers(),
}),
router: appRouter,
queryClient: getQueryClient,
});
 
// If your router is on a separate server, pass a client instead:
// createTRPCOptionsProxy({
// client: createTRPCClient({ links: [httpLink({ url: '...' })] }),
// queryClient: getQueryClient,
// });
trpc/server.tsx
tsx
import 'server-only'; // <-- ensure this file cannot be imported from the client
 
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import { headers } from 'next/headers';
import { cache } from 'react';
import { createTRPCContext } from './init';
import { makeQueryClient } from './query-client';
import { appRouter } from './routers/_app';
 
// IMPORTANT: Create a stable getter for the query client that
// will return the same client during the same request.
export const getQueryClient = cache(makeQueryClient);
 
export const trpc = createTRPCOptionsProxy({
ctx: async () =>
createTRPCContext({
headers: await headers(),
}),
router: appRouter,
queryClient: getQueryClient,
});
 
// If your router is on a separate server, pass a client instead:
// createTRPCOptionsProxy({
// client: createTRPCClient({ links: [httpLink({ url: '...' })] }),
// queryClient: getQueryClient,
// });

7. Gör API-anrop

Allt är klart! Du kan nu förhämta queries i serverkomponenter och använda dem i klientkomponenter.

Förhämtning i en serverkomponent

app/page.tsx
tsx
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { getQueryClient, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
const queryClient = getQueryClient();
void queryClient.prefetchQuery(
trpc.hello.queryOptions({
text: 'world',
}),
);
 
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ClientGreeting />
</HydrationBoundary>
);
}
app/page.tsx
tsx
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { getQueryClient, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
const queryClient = getQueryClient();
void queryClient.prefetchQuery(
trpc.hello.queryOptions({
text: 'world',
}),
);
 
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ClientGreeting />
</HydrationBoundary>
);
}

Använda data i en klientkomponent

app/client-greeting.tsx
tsx
'use client';
 
// <-- hooks can only be used in client components
import { useQuery } from '@tanstack/react-query';
import { useTRPC } from '~/trpc/client';
 
export function ClientGreeting() {
const trpc = useTRPC();
const greeting = useQuery(trpc.hello.queryOptions({ text: 'world' }));
if (!greeting.data) return <div>Loading...</div>;
return <div>{greeting.data.greeting}</div>;
}
app/client-greeting.tsx
tsx
'use client';
 
// <-- hooks can only be used in client components
import { useQuery } from '@tanstack/react-query';
import { useTRPC } from '~/trpc/client';
 
export function ClientGreeting() {
const trpc = useTRPC();
const greeting = useQuery(trpc.hello.queryOptions({ text: 'world' }));
if (!greeting.data) return <div>Loading...</div>;
return <div>{greeting.data.greeting}</div>;
}
tips

Du kan skapa hjälpfunktionerna prefetch och HydrateClient för att göra detta mer koncist:

trpc/server.tsx
tsx
export function HydrateClient(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<HydrationBoundary state={dehydrate(queryClient)}>
{props.children}
</HydrationBoundary>
);
}
 
export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(
queryOptions: T,
) {
const queryClient = getQueryClient();
if (queryOptions.queryKey[1]?.type === 'infinite') {
void queryClient.prefetchInfiniteQuery(queryOptions as any);
} else {
void queryClient.prefetchQuery(queryOptions);
}
}
trpc/server.tsx
tsx
export function HydrateClient(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<HydrationBoundary state={dehydrate(queryClient)}>
{props.children}
</HydrationBoundary>
);
}
 
export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(
queryOptions: T,
) {
const queryClient = getQueryClient();
if (queryOptions.queryKey[1]?.type === 'infinite') {
void queryClient.prefetchInfiniteQuery(queryOptions as any);
} else {
void queryClient.prefetchQuery(queryOptions);
}
}

Sedan kan du använda det så här:

tsx
import { HydrateClient, prefetch, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
function Home() {
prefetch(trpc.hello.queryOptions({ text: 'world' }));
 
return (
<HydrateClient>
<ClientGreeting />
</HydrateClient>
);
}
tsx
import { HydrateClient, prefetch, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
function Home() {
prefetch(trpc.hello.queryOptions({ text: 'world' }));
 
return (
<HydrateClient>
<ClientGreeting />
</HydrateClient>
);
}

Utnyttja Suspense

Du kan föredra att hantera laddnings- och felstatusar med Suspense och Error Boundaries. Det gör du med useSuspenseQuery-hooken.

app/page.tsx
tsx
import { HydrateClient, prefetch, trpc } from '~/trpc/server';
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
prefetch(trpc.hello.queryOptions());
 
return (
<HydrateClient>
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense fallback={<div>Loading...</div>}>
<ClientGreeting />
</Suspense>
</ErrorBoundary>
</HydrateClient>
);
}
app/page.tsx
tsx
import { HydrateClient, prefetch, trpc } from '~/trpc/server';
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
prefetch(trpc.hello.queryOptions());
 
return (
<HydrateClient>
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense fallback={<div>Loading...</div>}>
<ClientGreeting />
</Suspense>
</ErrorBoundary>
</HydrateClient>
);
}
app/client-greeting.tsx
tsx
'use client';
 
import { useSuspenseQuery } from '@tanstack/react-query';
import { useTRPC } from '~/trpc/client';
 
export function ClientGreeting() {
const trpc = useTRPC();
const { data } = useSuspenseQuery(trpc.hello.queryOptions());
return <div>{data.greeting}</div>;
}
app/client-greeting.tsx
tsx
'use client';
 
import { useSuspenseQuery } from '@tanstack/react-query';
import { useTRPC } from '~/trpc/client';
 
export function ClientGreeting() {
const trpc = useTRPC();
const { data } = useSuspenseQuery(trpc.hello.queryOptions());
return <div>{data.greeting}</div>;
}

Hämta data i en serverkomponent

Om du behöver åtkomst till data i en serverkomponent rekommenderar vi att skapa en servercaller och använda den direkt. Observera att denna metod är frikopplad från din query client och inte lagrar datan i cachen. Det innebär att du inte kan använda datan i en serverkomponent och förvänta dig att den ska finnas tillgänglig i klienten. Detta är avsiktligt och förklaras mer detaljerat i guiden om Avancerad serverrendering.

trpc/server.tsx
tsx
import { headers } from 'next/headers';
import { createTRPCContext } from './init';
import { appRouter } from './routers/_app';
 
// ...
export const caller = appRouter.createCaller(async () =>
createTRPCContext({ headers: await headers() }),
);
trpc/server.tsx
tsx
import { headers } from 'next/headers';
import { createTRPCContext } from './init';
import { appRouter } from './routers/_app';
 
// ...
export const caller = appRouter.createCaller(async () =>
createTRPCContext({ headers: await headers() }),
);
app/page.tsx
tsx
import { caller } from '~/trpc/server';
 
export default async function Home() {
const greeting = await caller.hello();
const greeting: { greeting: string; }
 
return <div>{greeting.greeting}</div>;
}
app/page.tsx
tsx
import { caller } from '~/trpc/server';
 
export default async function Home() {
const greeting = await caller.hello();
const greeting: { greeting: string; }
 
return <div>{greeting.greeting}</div>;
}

Om du verkligen behöver använda datan både på servern och i klientkomponenter och förstår kompromisserna som beskrivs i guiden Avancerad serverrendering, kan du använda fetchQuery istället för prefetch för att ha datan både på servern och hydrera den till klienten:

app/page.tsx
tsx
import { getQueryClient, HydrateClient, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
const queryClient = getQueryClient();
const greeting = await queryClient.fetchQuery(trpc.hello.queryOptions());
 
// Do something with greeting on the server
 
return (
<HydrateClient>
<ClientGreeting />
</HydrateClient>
);
}
app/page.tsx
tsx
import { getQueryClient, HydrateClient, trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
 
export default async function Home() {
const queryClient = getQueryClient();
const greeting = await queryClient.fetchQuery(trpc.hello.queryOptions());
 
// Do something with greeting on the server
 
return (
<HydrateClient>
<ClientGreeting />
</HydrateClient>
);
}

Nästa steg