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 →
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/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. 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. Aquí importa la definición de tipos
de tu router tRPC y crea hooks tipados usando createTRPCContext. 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 { 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>);}
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 hacer prefetching de queries desde componentes servidor, creamos un proxy desde nuestro router. También puedes pasar un cliente si tu router está en un servidor separado.
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,});
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.
Este enfoque aprovecha las capacidades de streaming del App Router de Next.js, iniciando la consulta en el servidor y transmitiendo datos al cliente según disponibilidad. Optimiza tanto el tiempo hasta el primer byte en el navegador como la obtención de datos, logrando cargas más rápidas. Sin embargo, greeting.data podría ser inicialmente undefined antes de que lleguen los datos.
Si prefieres evitar este estado inicial undefined, puedes usar await con la llamada prefetchQuery. Esto garantiza que el cliente siempre tenga datos en el primer renderizado, pero con una desventaja: la página cargará más lento ya que el servidor debe completar la consulta antes de enviar HTML al cliente.
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>;}
También puedes crear funciones auxiliares prefetch y HydrateClient para mayor concisión y reusabilidad:
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);}}
Luego lo usas así:
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>);}
Aprovechando Suspense
Puedes manejar estados de carga y error usando Suspense y Error Boundaries mediante el 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>;}
Obteniendo datos en un componente del servidor
Si necesitas acceder a datos en un componente del servidor, recomendamos crear un server caller y usarlo directamente. Nota que este método está desacoplado del query client y no almacena datos en caché. Esto significa que no puedes usar datos en un componente del servidor y esperarlos en el cliente. Es intencional y se explica en la guía Advanced Server Rendering.
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 realmente necesitas usar datos tanto en servidor como en cliente y entiendes las desventajas explicadas en la guía Advanced Server Rendering, usa fetchQuery en lugar de prefetch para tener datos en el servidor e hidratarlos en el cliente:
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>);}