Configuration avec Next.js App Router
Cette page a été traduite par PageTurner AI (bêta). Non approuvée officiellement par le projet. Vous avez trouvé une erreur ? Signaler un problème →
Nous vous recommandons de consulter la documentation de TanStack React Query sur le Rendu serveur avancé pour comprendre les différents types de rendu serveur et les pièges à éviter.
Structure de fichiers recommandée
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│ └── [..]└── [..]
Ajouter tRPC à un projet Next.js App Router existant
1. Installer les dépendances
- npm
- yarn
- pnpm
- bun
- deno
npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only
yarn add @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only
pnpm add @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only
bun add @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only
deno add npm:@trpc/server npm:@trpc/client npm:@trpc/tanstack-react-query npm:@tanstack/react-query@latest npm:zod npm:client-only npm:server-only
Si vous utilisez un agent de codage IA, installez les compétences tRPC pour une meilleure génération de code :
bashnpx @tanstack/intent@latest install
bashnpx @tanstack/intent@latest install
2. Créer un routeur tRPC
Initialisez votre backend tRPC dans trpc/init.ts en utilisant la fonction initTRPC, et créez votre premier routeur. Nous allons créer un simple routeur et procédure "hello world" ici - pour des informations plus approfondies sur la création de votre API tRPC, référez-vous au Guide de démarrage rapide et à la documentation Usage backend.
trpc/init.tstsimport {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 constcreateTRPCContext = 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.constt =initTRPC .context <Awaited <ReturnType <typeofcreateTRPCContext >>>().create ({/*** @see https://trpc.io/docs/server/data-transformers*/// transformer: superjson,});// Base router and procedure helpersexport constcreateTRPCRouter =t .router ;export constcreateCallerFactory =t .createCallerFactory ;export constbaseProcedure =t .procedure ;
trpc/init.tstsimport {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 constcreateTRPCContext = 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.constt =initTRPC .context <Awaited <ReturnType <typeofcreateTRPCContext >>>().create ({/*** @see https://trpc.io/docs/server/data-transformers*/// transformer: superjson,});// Base router and procedure helpersexport constcreateTRPCRouter =t .router ;export constcreateCallerFactory =t .createCallerFactory ;export constbaseProcedure =t .procedure ;
trpc/routers/_app.tstsimport {z } from 'zod';import {baseProcedure ,createTRPCRouter } from '../init';export constappRouter =createTRPCRouter ({hello :baseProcedure .input (z .object ({text :z .string (),}),).query ((opts ) => {return {greeting : `hello ${opts .input .text }`,};}),});// export type definition of APIexport typeAppRouter = typeofappRouter ;
trpc/routers/_app.tstsimport {z } from 'zod';import {baseProcedure ,createTRPCRouter } from '../init';export constappRouter =createTRPCRouter ({hello :baseProcedure .input (z .object ({text :z .string (),}),).query ((opts ) => {return {greeting : `hello ${opts .input .text }`,};}),});// export type definition of APIexport typeAppRouter = typeofappRouter ;
3. Créer le gestionnaire de route d'API
Avec l'App Router, utilisez l'adaptateur fetch pour gérer les requêtes tRPC. Créez un gestionnaire de route qui exporte à la fois GET et POST :
app/api/trpc/[trpc]/route.tstsimport {fetchRequestHandler } from '@trpc/server/adapters/fetch';import {createTRPCContext } from './trpc/init';import {appRouter } from './trpc/routers/_app';consthandler = (req :Request ) =>fetchRequestHandler ({endpoint : '/api/trpc',req ,router :appRouter ,createContext : () =>createTRPCContext ({headers :req .headers }),});export {handler asGET ,handler asPOST };
app/api/trpc/[trpc]/route.tstsimport {fetchRequestHandler } from '@trpc/server/adapters/fetch';import {createTRPCContext } from './trpc/init';import {appRouter } from './trpc/routers/_app';consthandler = (req :Request ) =>fetchRequestHandler ({endpoint : '/api/trpc',req ,router :appRouter ,createContext : () =>createTRPCContext ({headers :req .headers }),});export {handler asGET ,handler asPOST };
L'App Router utilise l'adaptateur fetch (via fetchRequestHandler) plutôt que l'adaptateur spécifique à Next.js utilisé par le Pages Router. Cela est dû au fait que les gestionnaires de route de l'App Router sont basés sur les objets standards Request et Response.
4. Créer une fabrique de Query Client
Créez un fichier partagé trpc/query-client.ts qui exporte une fonction générant une instance de QueryClient.
trpc/query-client.tstsimport {defaultShouldDehydrateQuery ,QueryClient ,} from '@tanstack/react-query';importsuperjson from 'superjson';export functionmakeQueryClient () {return newQueryClient ({defaultOptions : {queries : {staleTime : 30 * 1000,},dehydrate : {// serializeData: superjson.serialize,shouldDehydrateQuery : (query ) =>defaultShouldDehydrateQuery (query ) ||query .state .status === 'pending',},hydrate : {// deserializeData: superjson.deserialize,},},});}
trpc/query-client.tstsimport {defaultShouldDehydrateQuery ,QueryClient ,} from '@tanstack/react-query';importsuperjson from 'superjson';export functionmakeQueryClient () {return newQueryClient ({defaultOptions : {queries : {staleTime : 30 * 1000,},dehydrate : {// serializeData: superjson.serialize,shouldDehydrateQuery : (query ) =>defaultShouldDehydrateQuery (query ) ||query .state .status === 'pending',},hydrate : {// deserializeData: superjson.deserialize,},},});}
Nous définissons quelques options par défaut :
-
staleTime: Avec le SSR, nous définissons généralement une staleTime par défaut supérieure à 0 pour éviter un rechargement immédiat côté client. -
shouldDehydrateQuery: Cette fonction détermine si une requête doit être déshydratée. Comme le protocole RSC supporte l'hydratation des promesses via le réseau, nous étendons la fonctiondefaultShouldDehydrateQuerypour inclure aussi les requêtes encore en attente. Cela permet de démarrer le préchargement dans un composant serveur haut dans l'arbre, puis de consommer cette promesse dans un composant client plus bas. -
serializeDataetdeserializeData(optionnel) : Si vous avez configuré un transformateur de données à l'étape précédente, définissez cette option pour garantir une sérialisation correcte lors de l'hydratation du Query Client à travers la frontière serveur-client.
5. Créer un client tRPC pour les composants clients
Le fichier trpc/client.tsx sert de point d'entrée pour consommer votre API tRPC depuis les composants clients. Importez ici la définition de type de votre routeur tRPC et créez des hooks typés avec createTRPCContext. Nous exporterons également notre fournisseur de contexte depuis ce fichier.
trpc/client.tsxtsx'use client';// ^-- to make sure we can mount the Provider from a server componentimport 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 >();letbrowserQueryClient :QueryClient ;functiongetQueryClient () {if (typeofwindow === 'undefined') {// Server: always make a new query clientreturnmakeQueryClient ();}// 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 clientif (!browserQueryClient )browserQueryClient =makeQueryClient ();returnbrowserQueryClient ;}functiongetUrl () {constbase = (() => {if (typeofwindow !== 'undefined') return '';if (process .env .VERCEL_URL ) return `https://${process .env .VERCEL_URL }`;return 'http://localhost:3000';})();return `${base }/api/trpc`;}export functionTRPCReactProvider (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 boundaryconstqueryClient =getQueryClient ();const [trpcClient ] =useState (() =>createTRPCClient <AppRouter >({links : [httpBatchLink ({// transformer: superjson, <-- if you use a data transformerurl :getUrl (),}),],}),);return (<QueryClientProvider client ={queryClient }><TRPCProvider trpcClient ={trpcClient }queryClient ={queryClient }>{props .children }</TRPCProvider ></QueryClientProvider >);}
trpc/client.tsxtsx'use client';// ^-- to make sure we can mount the Provider from a server componentimport 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 >();letbrowserQueryClient :QueryClient ;functiongetQueryClient () {if (typeofwindow === 'undefined') {// Server: always make a new query clientreturnmakeQueryClient ();}// 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 clientif (!browserQueryClient )browserQueryClient =makeQueryClient ();returnbrowserQueryClient ;}functiongetUrl () {constbase = (() => {if (typeofwindow !== 'undefined') return '';if (process .env .VERCEL_URL ) return `https://${process .env .VERCEL_URL }`;return 'http://localhost:3000';})();return `${base }/api/trpc`;}export functionTRPCReactProvider (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 boundaryconstqueryClient =getQueryClient ();const [trpcClient ] =useState (() =>createTRPCClient <AppRouter >({links : [httpBatchLink ({// transformer: superjson, <-- if you use a data transformerurl :getUrl (),}),],}),);return (<QueryClientProvider client ={queryClient }><TRPCProvider trpcClient ={trpcClient }queryClient ={queryClient }>{props .children }</TRPCProvider ></QueryClientProvider >);}
Montez le fournisseur dans la mise en page racine de votre application :
app/layout.tsxtsximport {TRPCReactProvider } from '~/trpc/client';export default functionRootLayout ({children ,}:Readonly <{children :React .ReactNode ;}>) {return (<html lang ="en"><body ><TRPCReactProvider >{children }</TRPCReactProvider ></body ></html >);}
app/layout.tsxtsximport {TRPCReactProvider } from '~/trpc/client';export default functionRootLayout ({children ,}:Readonly <{children :React .ReactNode ;}>) {return (<html lang ="en"><body ><TRPCReactProvider >{children }</TRPCReactProvider ></body ></html >);}
6. Créer un appelant tRPC pour les composants serveur
Pour précharger les requêtes depuis les composants serveur, nous créons un proxy à partir de notre routeur. Vous pouvez également passer un client si votre routeur est sur un serveur distinct.
trpc/server.tsxtsximport 'server-only'; // <-- ensure this file cannot be imported from the clientimport {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 constgetQueryClient =cache (makeQueryClient );export consttrpc =createTRPCOptionsProxy ({ctx : async () =>createTRPCContext ({headers : awaitheaders (),}),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.tsxtsximport 'server-only'; // <-- ensure this file cannot be imported from the clientimport {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 constgetQueryClient =cache (makeQueryClient );export consttrpc =createTRPCOptionsProxy ({ctx : async () =>createTRPCContext ({headers : awaitheaders (),}),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. Effectuer des requêtes d'API
Vous êtes prêt ! Vous pouvez désormais précharger les requêtes dans les composants serveur et les consommer dans les composants clients.
Préchargement dans un composant serveur
app/page.tsxtsximport {dehydrate ,HydrationBoundary } from '@tanstack/react-query';import {getQueryClient ,trpc } from '~/trpc/server';import {ClientGreeting } from './client-greeting';export default async functionHome () {constqueryClient =getQueryClient ();voidqueryClient .prefetchQuery (trpc .hello .queryOptions ({text : 'world',}),);return (<HydrationBoundary state ={dehydrate (queryClient )}><ClientGreeting /></HydrationBoundary >);}
app/page.tsxtsximport {dehydrate ,HydrationBoundary } from '@tanstack/react-query';import {getQueryClient ,trpc } from '~/trpc/server';import {ClientGreeting } from './client-greeting';export default async functionHome () {constqueryClient =getQueryClient ();voidqueryClient .prefetchQuery (trpc .hello .queryOptions ({text : 'world',}),);return (<HydrationBoundary state ={dehydrate (queryClient )}><ClientGreeting /></HydrationBoundary >);}
Utilisation des données dans un composant client
app/client-greeting.tsxtsx'use client';// <-- hooks can only be used in client componentsimport {useQuery } from '@tanstack/react-query';import {useTRPC } from '~/trpc/client';export functionClientGreeting () {consttrpc =useTRPC ();constgreeting =useQuery (trpc .hello .queryOptions ({text : 'world' }));if (!greeting .data ) return <div >Loading...</div >;return <div >{greeting .data .greeting }</div >;}
app/client-greeting.tsxtsx'use client';// <-- hooks can only be used in client componentsimport {useQuery } from '@tanstack/react-query';import {useTRPC } from '~/trpc/client';export functionClientGreeting () {consttrpc =useTRPC ();constgreeting =useQuery (trpc .hello .queryOptions ({text : 'world' }));if (!greeting .data ) return <div >Loading...</div >;return <div >{greeting .data .greeting }</div >;}
Vous pouvez créer des fonctions d'aide prefetch et HydrateClient pour rendre cela plus concis :
trpc/server.tsxtsxexport functionHydrateClient (props : {children :React .ReactNode }) {constqueryClient =getQueryClient ();return (<HydrationBoundary state ={dehydrate (queryClient )}>{props .children }</HydrationBoundary >);}export functionprefetch <T extendsReturnType <TRPCQueryOptions <any>>>(queryOptions :T ,) {constqueryClient =getQueryClient ();if (queryOptions .queryKey [1]?.type === 'infinite') {voidqueryClient .prefetchInfiniteQuery (queryOptions as any);} else {voidqueryClient .prefetchQuery (queryOptions );}}
trpc/server.tsxtsxexport functionHydrateClient (props : {children :React .ReactNode }) {constqueryClient =getQueryClient ();return (<HydrationBoundary state ={dehydrate (queryClient )}>{props .children }</HydrationBoundary >);}export functionprefetch <T extendsReturnType <TRPCQueryOptions <any>>>(queryOptions :T ,) {constqueryClient =getQueryClient ();if (queryOptions .queryKey [1]?.type === 'infinite') {voidqueryClient .prefetchInfiniteQuery (queryOptions as any);} else {voidqueryClient .prefetchQuery (queryOptions );}}
Vous pouvez ensuite les utiliser ainsi :
tsximport {HydrateClient ,prefetch ,trpc } from '~/trpc/server';import {ClientGreeting } from './client-greeting';functionHome () {prefetch (trpc .hello .queryOptions ({text : 'world' }));return (<HydrateClient ><ClientGreeting /></HydrateClient >);}
tsximport {HydrateClient ,prefetch ,trpc } from '~/trpc/server';import {ClientGreeting } from './client-greeting';functionHome () {prefetch (trpc .hello .queryOptions ({text : 'world' }));return (<HydrateClient ><ClientGreeting /></HydrateClient >);}
Tirer parti de Suspense
Vous pouvez préférer gérer les états de chargement et d'erreur via Suspense et les Error Boundaries en utilisant le hook useSuspenseQuery.
app/page.tsxtsximport {HydrateClient ,prefetch ,trpc } from '~/trpc/server';import {Suspense } from 'react';import {ErrorBoundary } from 'react-error-boundary';import {ClientGreeting } from './client-greeting';export default async functionHome () {prefetch (trpc .hello .queryOptions ());return (<HydrateClient ><ErrorBoundary fallback ={<div >Something went wrong</div >}><Suspense fallback ={<div >Loading...</div >}><ClientGreeting /></Suspense ></ErrorBoundary ></HydrateClient >);}
app/page.tsxtsximport {HydrateClient ,prefetch ,trpc } from '~/trpc/server';import {Suspense } from 'react';import {ErrorBoundary } from 'react-error-boundary';import {ClientGreeting } from './client-greeting';export default async functionHome () {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.tsxtsx'use client';import {useSuspenseQuery } from '@tanstack/react-query';import {useTRPC } from '~/trpc/client';export functionClientGreeting () {consttrpc =useTRPC ();const {data } =useSuspenseQuery (trpc .hello .queryOptions ());return <div >{data .greeting }</div >;}
app/client-greeting.tsxtsx'use client';import {useSuspenseQuery } from '@tanstack/react-query';import {useTRPC } from '~/trpc/client';export functionClientGreeting () {consttrpc =useTRPC ();const {data } =useSuspenseQuery (trpc .hello .queryOptions ());return <div >{data .greeting }</div >;}
Obtenir des données dans un composant serveur
Si vous avez besoin d'accéder aux données dans un composant serveur, nous recommandons de créer un appelant serveur et de l'utiliser directement. Notez que cette méthode est découplée de votre Query Client et ne stocke pas les données dans le cache. Cela signifie que vous ne pouvez pas utiliser les données dans un composant serveur et les retrouver côté client. Ce comportement est intentionnel et expliqué en détail dans le guide Rendu serveur avancé.
trpc/server.tsxtsximport {headers } from 'next/headers';import {createTRPCContext } from './init';import {appRouter } from './routers/_app';// ...export constcaller =appRouter .createCaller (async () =>createTRPCContext ({headers : awaitheaders () }),);
trpc/server.tsxtsximport {headers } from 'next/headers';import {createTRPCContext } from './init';import {appRouter } from './routers/_app';// ...export constcaller =appRouter .createCaller (async () =>createTRPCContext ({headers : awaitheaders () }),);
app/page.tsxtsximport {caller } from '~/trpc/server';export default async functionHome () {constgreeting = awaitcaller .hello ();return <div >{greeting .greeting }</div >;}
app/page.tsxtsximport {caller } from '~/trpc/server';export default async functionHome () {constgreeting = awaitcaller .hello ();return <div >{greeting .greeting }</div >;}
Si vous avez vraiment besoin d'utiliser les données à la fois sur le serveur et dans les composants clients, et que vous comprenez les compromis expliqués dans le guide du
Rendu serveur avancé,
vous pouvez utiliser fetchQuery au lieu de prefetch pour obtenir les données sur le serveur tout en les hydratant côté client :
app/page.tsxtsximport {getQueryClient ,HydrateClient ,trpc } from '~/trpc/server';import {ClientGreeting } from './client-greeting';export default async functionHome () {constqueryClient =getQueryClient ();constgreeting = awaitqueryClient .fetchQuery (trpc .hello .queryOptions ());// Do something with greeting on the serverreturn (<HydrateClient ><ClientGreeting /></HydrateClient >);}
app/page.tsxtsximport {getQueryClient ,HydrateClient ,trpc } from '~/trpc/server';import {ClientGreeting } from './client-greeting';export default async functionHome () {constqueryClient =getQueryClient ();constgreeting = awaitqueryClient .fetchQuery (trpc .hello .queryOptions ());// Do something with greeting on the serverreturn (<HydrateClient ><ClientGreeting /></HydrateClient >);}
Prochaines étapes
-
Découvrez les Server Actions pour définir des actions serveur propulsées par tRPC
-
Apprenez-en davantage sur les queries et les mutations dans les composants clients
-
Explorez les appels côté serveur pour des modèles plus avancés
-
Consultez l'exemple SSE Chat pour un exemple complet avec l'App Router et des abonnements