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​