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 →
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 de solution universelle pour intégrer tRPC aux RSC. Considérez ce guide comme un point de départ et adaptez-le à vos besoins et préférences.
Si vous cherchez comment utiliser tRPC avec les Server Actions, consultez cet article de blog par Julius.
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 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 Utilisation 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 const createTRPCContext = 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.const t = initTRPC.create({/*** @see https://trpc.io/docs/server/data-transformers*/// transformer: superjson,});// Base router and procedure helpersexport const createTRPCRouter = t.router;export const createCallerFactory = t.createCallerFactory;export const baseProcedure = t.procedure;
trpc/init.tstsimport { initTRPC } from '@trpc/server';import { cache } from 'react';export const createTRPCContext = 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.const t = initTRPC.create({/*** @see https://trpc.io/docs/server/data-transformers*/// transformer: superjson,});// Base router and procedure helpersexport const createTRPCRouter = t.router;export const createCallerFactory = t.createCallerFactory;export const baseProcedure = t.procedure;
trpc/routers/_app.tstsimport { 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 APIexport type AppRouter = typeof appRouter;
trpc/routers/_app.tstsimport { 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 APIexport type AppRouter = typeof appRouter;
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';const handler = (req: Request) =>fetchRequestHandler({endpoint: '/api/trpc',req,router: appRouter,createContext: createTRPCContext,});export { handler as GET, handler as POST };
app/api/trpc/[trpc]/route.tstsimport { 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,});export { handler as GET, handler as POST };
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';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.tstsimport {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,},},});}
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>();let browserQueryClient: QueryClient;function getQueryClient() {if (typeof window === 'undefined') {// Server: always make a new query clientreturn 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 clientif (!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 boundaryconst queryClient = 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>();let browserQueryClient: QueryClient;function getQueryClient() {if (typeof window === 'undefined') {// Server: always make a new query clientreturn 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 clientif (!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 boundaryconst queryClient = 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 { 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: createTRPCContext,router: appRouter,queryClient: getQueryClient,});// If your router is on a separate server, pass a client: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 { 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: createTRPCContext,router: appRouter,queryClient: getQueryClient,});// If your router is on a separate server, pass a client:createTRPCOptionsProxy({client: createTRPCClient({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 function Home() {const queryClient = getQueryClient();void queryClient.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 function Home() {const queryClient = getQueryClient();void queryClient.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 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.tsxtsx'use client';// <-- hooks can only be used in client componentsimport { 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>;}
Vous pouvez aussi créer des fonctions utilitaires prefetch et HydrateClient pour plus de concision et réutilisabilité :
trpc/server.tsxtsxexport 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.tsxtsxexport 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);}}
Puis les utiliser ainsi :
tsximport { HydrateClient, prefetch, trpc } from '~/trpc/server';function Home() {prefetch(trpc.hello.queryOptions({/** input */}),);return (<HydrateClient><div>...</div>{/** ... */}<ClientGreeting /></HydrateClient>);}
tsximport { HydrateClient, prefetch, trpc } from '~/trpc/server';function Home() {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 function Home() {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 function Home() {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 { trpc } from '~/trpc/client';export function ClientGreeting() {const trpc = 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 { trpc } from '~/trpc/client';export function ClientGreeting() {const trpc = 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 const caller = appRouter.createCaller(createTRPCContext);
trpc/server.tsxtsx// ...export const caller = appRouter.createCaller(createTRPCContext);
app/page.tsxtsximport { caller } from '~/trpc/server';export default async function Home() {const greeting = await caller.hello();// ^? { greeting: string }return <div>{greeting.greeting}</div>;}
app/page.tsxtsximport { caller } from '~/trpc/server';export default async function Home() {const greeting = await caller.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';export default async function Home() {const queryClient = getQueryClient();const greeting = await queryClient.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';export default async function Home() {const queryClient = getQueryClient();const greeting = await queryClient.fetchQuery(trpc.hello.queryOptions());// Do something with greeting on the serverreturn (<HydrateClient><div>...</div>{/** ... */}<ClientGreeting /></HydrateClient>);}