Saltar al contenido principal
Versión: 11.x

Configuración con React Server Components

Traducción Beta No Oficial

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.

información

Si buscas cómo usar tRPC con Server Actions, consulta esta publicación de Julius.

precaución

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 install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only 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.

información

Los nombres de archivo usados aquí no son obligatorios en tRPC. Puedes usar cualquier estructura de archivos que prefieras.

View sample backend
trpc/init.ts
ts
import { 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 helpers
export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;
export const baseProcedure = t.procedure;
trpc/init.ts
ts
import { 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 helpers
export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;
export const baseProcedure = t.procedure;

trpc/routers/_app.ts
ts
import { 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 API
export type AppRouter = typeof appRouter;
trpc/routers/_app.ts
ts
import { 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 API
export type AppRouter = typeof appRouter;

nota

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.ts
ts
import { 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.ts
ts
import { 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.ts
ts
import {
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.ts
ts
import {
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ón defaultShouldDehydrateQuery para 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.

  • serializeData y deserializeData (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.tsx
tsx
'use client';
// ^-- to make sure we can mount the Provider from a server component
import 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 client
return 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 client
if (!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 boundary
const queryClient = getQueryClient();
const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({
links: [
httpBatchLink({
// transformer: superjson, <-- if you use a data transformer
url: getUrl(),
}),
],
}),
);
return (
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
{props.children}
</TRPCProvider>
</QueryClientProvider>
);
}
trpc/client.tsx
tsx
'use client';
// ^-- to make sure we can mount the Provider from a server component
import 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 client
return 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 client
if (!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 boundary
const queryClient = getQueryClient();
const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({
links: [
httpBatchLink({
// transformer: superjson, <-- if you use a data transformer
url: 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.tsx
tsx
import 'server-only'; // <-- ensure this file cannot be imported from the client
import { 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.tsx
tsx
import 'server-only'; // <-- ensure this file cannot be imported from the client
import { 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.tsx
tsx
import { 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.tsx
tsx
import { 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.tsx
tsx
'use client';
// <-- hooks can only be used in client components
import { 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.tsx
tsx
'use client';
// <-- hooks can only be used in client components
import { 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>;
}
consejo

También puedes crear funciones auxiliares prefetch y HydrateClient para mayor concisión y reusabilidad:

trpc/server.tsx
tsx
export 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.tsx
tsx
export 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í:

tsx
import { HydrateClient, prefetch, trpc } from '~/trpc/server';
function Home() {
prefetch(
trpc.hello.queryOptions({
/** input */
}),
);
return (
<HydrateClient>
<div>...</div>
{/** ... */}
<ClientGreeting />
</HydrateClient>
);
}
tsx
import { 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.tsx
tsx
import { 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.tsx
tsx
import { 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.tsx
tsx
'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.tsx
tsx
'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.tsx
tsx
// ...
export const caller = appRouter.createCaller(createTRPCContext);
trpc/server.tsx
tsx
// ...
export const caller = appRouter.createCaller(createTRPCContext);
app/page.tsx
tsx
import { caller } from '~/trpc/server';
export default async function Home() {
const greeting = await caller.hello();
// ^? { greeting: string }
return <div>{greeting.greeting}</div>;
}
app/page.tsx
tsx
import { 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.tsx
tsx
import { 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 server
return (
<HydrateClient>
<div>...</div>
{/** ... */}
<ClientGreeting />
</HydrateClient>
);
}
app/page.tsx
tsx
import { 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 server
return (
<HydrateClient>
<div>...</div>
{/** ... */}
<ClientGreeting />
</HydrateClient>
);
}