Configuration avec les composants serveur React
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 →
Vous utilisez Next.js ? Consultez notre guide dédié Configuration avec Next.js App Router pour une procédure optimisée spécifique à Next.js.
Ce guide présente comment utiliser tRPC avec un framework utilisant les composants serveur React (RSC) comme Next.js App Router. Notez que les RSC résolvent déjà de nombreux problèmes que tRPC était conçu pour traiter, donc vous pourriez ne pas avoir besoin de tRPC.
Il n'existe pas non plus de solution universelle pour intégrer tRPC avec les RSC. Considérez ce guide comme un point de départ et adaptez-le à vos besoins et préférences.
Si vous cherchez à utiliser tRPC avec les Server Actions, consultez le guide Server Actions.
Lisez impérativement la documentation Rendu serveur avancé de React Query avant de continuer, pour comprendre les différents types de rendu serveur et les pièges à éviter.
Ajouter tRPC à des projets existants
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
2. Créer un routeur tRPC
Initialisez votre backend tRPC dans trpc/init.ts avec la fonction initTRPC, puis créez votre premier routeur. Nous allons implémenter ici un routeur et une procédure simples de type "hello world" - mais pour des informations approfondies sur la création de votre API tRPC, reportez-vous au guide de démarrage rapide et à la documentation backend.
Les noms de fichiers utilisés ici ne sont pas imposés par tRPC. Vous pouvez adopter n'importe quelle structure de fichiers.
View sample backend
trpc/init.tstsimport {initTRPC } from '@trpc/server';import {cache } from 'react';export constcreateTRPCContext =cache (async () => {/*** @see: https://trpc.io/docs/server/context*/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 .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';import {cache } from 'react';export constcreateTRPCContext =cache (async () => {/*** @see: https://trpc.io/docs/server/context*/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 .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 ;
The backend adapter depends on your framework and how it sets up API routes. The following example sets up GET and POST routes at /api/trpc/* using the fetch adapter in Next.js.
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 ,});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 ,});export {handler asGET ,handler asPOST };
3. 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.
4. 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 à la racine de votre application (par exemple dans app/layout.tsx avec Next.js).
5. 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 {createTRPCClient ,httpLink } from '@trpc/client';import {cache } from 'react';import {createTRPCContext } from './init';import {makeQueryClient } from './query-client';import {appRouter } from './routers/_app';import type {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 :createTRPCContext ,router :appRouter ,queryClient :getQueryClient ,});// If your router is on a separate server, pass a client:createTRPCOptionsProxy <AppRouter >({client :createTRPCClient <AppRouter >({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 {createTRPCClient ,httpLink } from '@trpc/client';import {cache } from 'react';import {createTRPCContext } from './init';import {makeQueryClient } from './query-client';import {appRouter } from './routers/_app';import type {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 :createTRPCContext ,router :appRouter ,queryClient :getQueryClient ,});// If your router is on a separate server, pass a client:createTRPCOptionsProxy <AppRouter >({client :createTRPCClient <AppRouter >({links : [httpLink ({url : '...' })],}),queryClient :getQueryClient ,});
Utiliser votre API
Vous pouvez désormais utiliser votre API tRPC dans votre application. Si les hooks React Query fonctionnent dans les composants clients comme dans toute application React classique, nous pouvons exploiter les capacités RSC en préchargeant les requêtes dans un composant serveur haut dans l'arbre. Vous connaissez peut-être ce concept sous le nom de "render as you fetch" (rendu pendant le chargement), souvent implémenté via des loaders. Cela signifie que la requête est déclenchée le plus tôt possible sans suspension jusqu'à ce que les données soient nécessaires, en utilisant les hooks useQuery ou useSuspenseQuery.
Cette approche exploite les capacités de streaming du Next.js App Router, lançant la requête côté serveur et transmettant progressivement les données au client. Elle optimise à la fois le temps jusqu'au premier octet dans le navigateur et la durée de récupération des données, accélérant ainsi le chargement des pages. Notez cependant que greeting.data peut initialement être undefined avant l'arrivée des données streamées.
Si vous préférez éviter cet état initial indéfini, vous pouvez utiliser await sur l'appel prefetchQuery. Cela garantit que la requête côté client dispose toujours de données au premier rendu, mais avec un compromis : le chargement de la page sera plus lent car le serveur doit terminer la requête avant d'envoyer le HTML au client.
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 ({/** input */}),);return (<HydrationBoundary state ={dehydrate (queryClient )}><div >...</div >{/** ... */}<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 ({/** input */}),);return (<HydrationBoundary state ={dehydrate (queryClient )}><div >...</div >{/** ... */}<ClientGreeting /></HydrationBoundary >);}
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 aussi créer des fonctions d'aide prefetch et HydrateClient pour un code plus concis et réutilisable :
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 );}}
Ensuite, vous pouvez l'utiliser ainsi :
tsximport {HydrateClient ,prefetch ,trpc } from '../trpc/server';import {ClientGreeting } from './client-greeting';functionHome () {prefetch (trpc .hello .queryOptions ({/** input */}),);return (<HydrateClient ><div >...</div >{/** ... */}<ClientGreeting /></HydrateClient >);}
tsximport {HydrateClient ,prefetch ,trpc } from '../trpc/server';import {ClientGreeting } from './client-greeting';functionHome () {prefetch (trpc .hello .queryOptions ({/** input */}),);return (<HydrateClient ><div >...</div >{/** ... */}<ClientGreeting /></HydrateClient >);}
Exploiter 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 ><div >...</div >{/** ... */}<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 ><div >...</div >{/** ... */}<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 >;}
Récupérer 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.tsxtsx// ...export constcaller =appRouter .createCaller (createTRPCContext );
trpc/server.tsxtsx// ...export constcaller =appRouter .createCaller (createTRPCContext );
app/page.tsxtsximport {caller } from '../trpc/server';export default async functionHome () {constgreeting = awaitcaller .hello ();// ^? { greeting: string }return <div >{greeting .greeting }</div >;}
app/page.tsxtsximport {caller } from '../trpc/server';export default async functionHome () {constgreeting = awaitcaller .hello ();// ^? { greeting: string }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 ><div >...</div >{/** ... */}<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 ><div >...</div >{/** ... */}<ClientGreeting /></HydrateClient >);}