Configuración con React Server Components
Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
Estos son los documentos de nuestra integración 'Clásica' con React Query, que (aunque sigue siendo compatible) no es la forma recomendada para iniciar nuevos proyectos tRPC con TanStack React Query. Te recomendamos usar la nueva Integración con TanStack React Query en su lugar.
¿Usas Next.js? Consulta la guía de configuración de Next.js App Router dedicada para el enfoque recomendado.
Esta guía ofrece una visión general de cómo usar tRPC con un framework de React Server Components (RSC) como Next.js App Router. Ten en cuenta que RSC por sí mismo resuelve muchos de los problemas que tRPC fue diseñado para solucionar, por lo que quizás no necesites tRPC en absoluto.
Tampoco existe una solución única para integrar tRPC con RSC, así que considera esta guía como punto de partida y ajústala según tus necesidades y preferencias.
Si buscas cómo usar tRPC con Server Actions, consulta la guía de Server Actions.
Lee la documentación de Renderizado Avanzado en Servidor de React Query antes de continuar para entender los diferentes tipos de renderizado en servidor y qué trampas evitar.
Agregar tRPC a proyectos existentes
1. Instalar dependencias
- npm
- yarn
- pnpm
- bun
- deno
npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query@latest zod client-only server-only
yarn add @trpc/server @trpc/client @trpc/react-query @tanstack/react-query@latest zod client-only server-only
pnpm add @trpc/server @trpc/client @trpc/react-query @tanstack/react-query@latest zod client-only server-only
bun add @trpc/server @trpc/client @trpc/react-query @tanstack/react-query@latest zod client-only server-only
deno add npm:@trpc/server npm:@trpc/client npm:@trpc/react-query npm:@tanstack/react-query@latest npm:zod npm:client-only npm:server-only
2. Crear un router de tRPC
Inicializa tu backend de tRPC en trpc/init.ts usando la función initTRPC y crea tu primer router. Aquí crearemos un router y procedimiento simple de "hello world", pero para información más profunda sobre cómo crear tu API tRPC, consulta la guía de inicio rápido y la documentación de uso del backend.
Los nombres de archivo usados aquí no son obligatorios en tRPC. Puedes usar cualquier estructura de archivos que prefieras.
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. Crear una fábrica de Query Client
Crea un archivo compartido trpc/query-client.ts que exporte una función para crear instancias 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,},},});}
Establecemos algunas opciones predeterminadas:
-
staleTime: Con SSR, normalmente queremos establecer un staleTime predeterminado mayor a 0 para evitar recargas inmediatas en el cliente. -
shouldDehydrateQuery: Función que determina si una query debe deshidratarse. Como el protocolo de transporte RSC admite hidratar promesas a través de la red, extendemos la funcióndefaultShouldDehydrateQuerypara incluir también queries que aún están pendientes. Esto nos permite iniciar prefetching en un componente servidor alto en el árbol, luego consumir esa promesa en un componente cliente más abajo. -
serializeDataydeserializeData(opcional): Si configuraste un transformador de datos en el paso anterior, establece esta opción para asegurar que los datos se serialicen correctamente al hidratar el cliente de queries en el límite servidor-cliente.
4. Crear un cliente tRPC para componentes cliente
trpc/client.tsx es el punto de entrada al consumir tu API tRPC desde componentes cliente. Importa aquí la definición de tipos de tu enrutador tRPC y crea hooks tipados con createTRPCReact. También exportaremos nuestro proveedor de contexto desde este archivo.
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 {httpBatchLink } from '@trpc/client';import {createTRPCReact } from '@trpc/react-query';importReact , {useState } from 'react';import {makeQueryClient } from './query-client';import type {AppRouter } from './routers/_app';export consttrpc =createTRPCReact <AppRouter >();letclientQueryClientSingleton :QueryClient ;functiongetQueryClient () {if (typeofwindow === 'undefined') {// Server: always make a new query clientreturnmakeQueryClient ();}// Browser: use singleton pattern to keep the same query clientreturn (clientQueryClientSingleton ??=makeQueryClient ());}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 functionTRPCProvider (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 (() =>trpc .createClient ({links : [httpBatchLink ({// transformer: superjson, <-- if you use a data transformerurl :getUrl (),}),],}),);return (<trpc .Provider client ={trpcClient }queryClient ={queryClient }><QueryClientProvider client ={queryClient }>{props .children }</QueryClientProvider ></trpc .Provider >);}
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 {httpBatchLink } from '@trpc/client';import {createTRPCReact } from '@trpc/react-query';importReact , {useState } from 'react';import {makeQueryClient } from './query-client';import type {AppRouter } from './routers/_app';export consttrpc =createTRPCReact <AppRouter >();letclientQueryClientSingleton :QueryClient ;functiongetQueryClient () {if (typeofwindow === 'undefined') {// Server: always make a new query clientreturnmakeQueryClient ();}// Browser: use singleton pattern to keep the same query clientreturn (clientQueryClientSingleton ??=makeQueryClient ());}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 functionTRPCProvider (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 (() =>trpc .createClient ({links : [httpBatchLink ({// transformer: superjson, <-- if you use a data transformerurl :getUrl (),}),],}),);return (<trpc .Provider client ={trpcClient }queryClient ={queryClient }><QueryClientProvider client ={queryClient }>{props .children }</QueryClientProvider ></trpc .Provider >);}
Monta el proveedor en la raíz de tu aplicación (ej. app/layout.tsx en Next.js).
5. Crear un caller tRPC para componentes servidor
Para precargar consultas desde componentes servidor, usamos un llamador tRPC. El módulo @trpc/react-query/rsc exporta una capa delgada sobre createCaller que se integra con tu cliente React Query.
trpc/server.tsxtsximport 'server-only'; // <-- ensure this file cannot be imported from the clientimport {createHydrationHelpers } from '@trpc/react-query/rsc';import {cache } from 'react';import {createCallerFactory ,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 );constcaller =createCallerFactory (appRouter )(createTRPCContext );export const {trpc ,HydrateClient } =createHydrationHelpers <typeofappRouter >(caller ,getQueryClient ,);
trpc/server.tsxtsximport 'server-only'; // <-- ensure this file cannot be imported from the clientimport {createHydrationHelpers } from '@trpc/react-query/rsc';import {cache } from 'react';import {createCallerFactory ,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 );constcaller =createCallerFactory (appRouter )(createTRPCContext );export const {trpc ,HydrateClient } =createHydrationHelpers <typeofappRouter >(caller ,getQueryClient ,);
Usando tu API
Ahora puedes usar tu API de tRPC en tu aplicación. Si bien puedes usar los hooks de React Query en componentes del cliente como en cualquier otra app de React, podemos aprovechar las capacidades de RSC precargando consultas en un componente del servidor alto en el árbol. Tal vez conozcas este concepto como "render as you fetch" implementado comúnmente mediante loaders. Esto significa que la solicitud se dispara lo antes posible sin suspender hasta que los datos sean necesarios usando los hooks useQuery o useSuspenseQuery.
app/page.tsxtsximport {trpc ,HydrateClient } from '../trpc/server';import {ClientGreeting } from './client-greeting';export default async functionHome () {voidtrpc .hello .prefetch ();return (<HydrateClient ><div >...</div >{/** ... */}<ClientGreeting /></HydrateClient >);}
app/page.tsxtsximport {trpc ,HydrateClient } from '../trpc/server';import {ClientGreeting } from './client-greeting';export default async functionHome () {voidtrpc .hello .prefetch ();return (<HydrateClient ><div >...</div >{/** ... */}<ClientGreeting /></HydrateClient >);}
app/client-greeting.tsxtsx'use client';// <-- hooks can only be used in client componentsimport {trpc } from '../trpc/client';export functionClientGreeting () {constgreeting =trpc .hello .useQuery ();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 {trpc } from '../trpc/client';export functionClientGreeting () {constgreeting =trpc .hello .useQuery ();if (!greeting .data ) return <div >Loading...</div >;return <div >{greeting .data .greeting }</div >;}
Aprovechando Suspense
Puedes manejar estados de carga y error usando Suspense y Error Boundaries mediante el hook useSuspenseQuery.
app/page.tsxtsximport {trpc ,HydrateClient } from '../trpc/server';import {Suspense } from 'react';import {ErrorBoundary } from 'react-error-boundary';import {ClientGreeting } from './client-greeting';export default async functionHome () {voidtrpc .hello .prefetch ();return (<HydrateClient ><div >...</div >{/** ... */}<ErrorBoundary fallback ={<div >Something went wrong</div >}><Suspense fallback ={<div >Loading...</div >}><ClientGreeting /></Suspense ></ErrorBoundary ></HydrateClient >);}
app/page.tsxtsximport {trpc ,HydrateClient } from '../trpc/server';import {Suspense } from 'react';import {ErrorBoundary } from 'react-error-boundary';import {ClientGreeting } from './client-greeting';export default async functionHome () {voidtrpc .hello .prefetch ();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 {trpc } from '../trpc/client';export functionClientGreeting () {const [data ] =trpc .hello .useSuspenseQuery ();return <div >{data .greeting }</div >;}
app/client-greeting.tsxtsx'use client';import {trpc } from '../trpc/client';export functionClientGreeting () {const [data ] =trpc .hello .useSuspenseQuery ();return <div >{data .greeting }</div >;}
Obteniendo datos en un componente del servidor
Si necesitas acceder a los datos en un componente del servidor, puedes invocar el procedimiento directamente en lugar de usar .prefetch(), igual que usas el llamador normal del servidor. Ten en cuenta que este método está desvinculado de tu cliente de queries y no almacena los datos en la caché. Esto significa que no puedes usar los datos en un componente del servidor y esperar que estén disponibles en el cliente. Esto es intencional y se explica con más detalle en la guía de Renderizado Avanzado en Servidor.
app/page.tsxtsximport {trpc } from '../trpc/server';export default async functionHome () {// Use the caller directly without using `.prefetch()`constgreeting = awaittrpc .hello ();// ^? { greeting: string }return <div >{greeting .greeting }</div >;}
app/page.tsxtsximport {trpc } from '../trpc/server';export default async functionHome () {// Use the caller directly without using `.prefetch()`constgreeting = awaittrpc .hello ();// ^? { greeting: string }return <div >{greeting .greeting }</div >;}