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.
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 RSCs, así que considera esta guía como punto de partida y ajústala a tus necesidades y preferencias.
Si buscas cómo usar tRPC con Server Actions, consulta esta publicación de Julius.
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í implementaremos un router y procedimiento simple de "hola mundo", pero para información más profunda sobre cómo crear tu API con tRPC, consulta la guía rápida y la documentación de uso en 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 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. 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';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,},},});}
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';import { useState } from 'react';import { makeQueryClient } from './query-client';import type { AppRouter } from './routers/_app';export const trpc = createTRPCReact<AppRouter>();let clientQueryClientSingleton: QueryClient;function getQueryClient() {if (typeof window === 'undefined') {// Server: always make a new query clientreturn makeQueryClient();}// Browser: use singleton pattern to keep the same query clientreturn (clientQueryClientSingleton ??= makeQueryClient());}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 TRPCProvider(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(() =>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';import { useState } from 'react';import { makeQueryClient } from './query-client';import type { AppRouter } from './routers/_app';export const trpc = createTRPCReact<AppRouter>();let clientQueryClientSingleton: QueryClient;function getQueryClient() {if (typeof window === 'undefined') {// Server: always make a new query clientreturn makeQueryClient();}// Browser: use singleton pattern to keep the same query clientreturn (clientQueryClientSingleton ??= makeQueryClient());}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 TRPCProvider(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(() =>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 const getQueryClient = cache(makeQueryClient);const caller = createCallerFactory(appRouter)(createTRPCContext);export const { trpc, HydrateClient } = createHydrationHelpers<typeof appRouter>(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 const getQueryClient = cache(makeQueryClient);const caller = createCallerFactory(appRouter)(createTRPCContext);export const { trpc, HydrateClient } = createHydrationHelpers<typeof appRouter>(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 } from '~/trpc/server';import { ClientGreeting } from './client-greeting';export default async function Home() {void trpc.hello.prefetch();return (<HydrateClient><div>...</div>{/** ... */}<ClientGreeting /></HydrateClient>);}
app/page.tsxtsximport { trpc } from '~/trpc/server';import { ClientGreeting } from './client-greeting';export default async function Home() {void trpc.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 function ClientGreeting() {const greeting = 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 function ClientGreeting() {const greeting = 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 } from '~/trpc/server';import { Suspense } from 'react';import { ErrorBoundary } from 'react-error-boundary';import { ClientGreeting } from './client-greeting';export default async function Home() {void trpc.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 } from '~/trpc/server';import { Suspense } from 'react';import { ErrorBoundary } from 'react-error-boundary';import { ClientGreeting } from './client-greeting';export default async function Home() {void trpc.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 function ClientGreeting() {const [data] = trpc.hello.useSuspenseQuery();return <div>{data.greeting}</div>;}
app/client-greeting.tsxtsx'use client';import { trpc } from '~/trpc/client';export function ClientGreeting() {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 servidor, puedes invocar el procedimiento directamente en lugar de usar .prefetch(), igual que usarías el caller de servidor normal. Ten en cuenta que este método está desvinculado de tu query client y no almacena los datos en la caché. Esto significa que no puedes usar los datos en un componente 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 function Home() {// Use the caller directly without using `.prefetch()`const greeting = await trpc.hello();// ^? { greeting: string }return <div>{greeting.greeting}</div>;}
app/page.tsxtsximport { trpc } from '~/trpc/server';export default async function Home() {// Use the caller directly without using `.prefetch()`const greeting = await trpc.hello();// ^? { greeting: string }return <div>{greeting.greeting}</div>;}