跳至主内容
版本:11.x

在 React Server Components 中的配置方案

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

本指南概述了如何在 React Server Components (RSC) 框架(如 Next.js App Router)中使用 tRPC。需要注意的是,RSC 本身已解决了 tRPC 设计初衷要解决的许多问题,因此您可能完全不需要 tRPC。

此外,tRPC 与 RSC 的集成并非一刀切方案,请将此指南视为起点,并根据您的需求和偏好进行调整。

信息

如需了解如何在 Server Actions 中使用 tRPC,请参阅 Julius 的这篇博客文章

注意

请先阅读 React Query 的 高级服务端渲染 文档,了解不同类型的服务端渲染及需要规避的隐患。

在现有项目中添加 tRPC

1. 安装依赖

npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only

2. 创建 tRPC 路由

trpc/init.ts 中使用 initTRPC 函数初始化 tRPC 后端,并创建您的第一个路由。我们将在此创建简单的 "hello world" 路由和过程 - 如需深入了解如何创建 tRPC API,请参考 tRPC 的 快速入门指南后端使用文档

信息

此处使用的文件名并非 tRPC 强制要求,您可采用任意文件结构。

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;

备注

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. 创建 Query Client 工厂函数

创建共享文件 trpc/query-client.ts,导出用于创建 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,
},
},
});
}

我们在此设置若干默认选项:

  • staleTime:在 SSR 场景下,通常需设置大于 0 的默认 staleTime 以避免客户端立即重新获取数据

  • shouldDehydrateQuery:此函数决定查询是否应被脱水。由于 RSC 传输协议支持通过网络水合 promise,我们扩展了 defaultShouldDehydrateQuery 函数以包含仍在挂起的查询。这将允许我们在树形结构顶层的服务端组件启动预获取,并在下层的客户端组件消费该 promise

  • serializeDatadeserializeData(可选):若您在前一步设置了 数据转换器,请配置此选项以确保在跨越服务端-客户端边界水合 query client 时正确序列化数据

4. 为客户端组件创建 tRPC 客户端

trpc/client.tsx 是从客户端组件消费 tRPC API 的入口文件。在此导入 tRPC 路由的类型定义,并使用 createTRPCContext 创建类型安全的钩子。我们还将从此文件导出上下文提供者。

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>
);
}

将此提供者挂载到应用的根节点(例如使用 Next.js 时挂载到 app/layout.tsx)。

5. 为服务端组件创建 tRPC 调用器

要从服务端组件预获取查询,我们通过路由创建代理。若路由位于独立服务器,也可传递客户端实例。

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,
});

API 使用实践

现在你可以在应用中使用 tRPC API。虽然你可以在客户端组件中像其他 React 应用那样使用 React Query 钩子, 但我们还能利用 RSC 的特性:在高层级服务端组件中预取查询。你可能熟悉这种被称为"随取随渲"(render as you fetch)的模式, 它通常通过加载器实现。这意味着请求会尽早触发,但不会阻塞渲染,直到实际需要使用数据时才通过 useQueryuseSuspenseQuery 钩子挂起。

此方法利用了 Next.js App Router 的流式传输能力,在服务端发起查询并将数据流式传输到客户端。它同时优化了浏览器的首字节时间(TTFB) 和数据获取时间,从而提升页面加载速度。但需要注意:在数据流完全传输前,greeting.data 初始值可能是 undefined

若要避免这种初始未定义状态,可以在 prefetchQuery 调用前添加 await。 这能确保客户端首次渲染时总有可用数据,但存在权衡取舍——由于服务端必须完成查询才能发送 HTML 到客户端,页面加载时间会相应延长。

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>;
}
技巧

你可以创建 prefetchHydrateClient 辅助函数来简化代码并提高复用性:

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);
}
}

使用示例如下:

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>
);
}

利用 Suspense

你可以使用 useSuspenseQuery 钩子,通过 Suspense 和 Error Boundaries 来处理加载与错误状态。

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>;
}

在服务端组件获取数据

若需在服务端组件直接访问数据,建议创建服务端调用器(server caller)。请注意:此方法与查询客户端分离, 不会将数据存入缓存。这意味着无法在服务端使用数据后预期客户端能直接获取该数据。这种设计是刻意为之, 详细原理请参阅高级服务端渲染指南。

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>;
}

若你确实需要同时在服务端和客户端组件使用数据,并理解高级服务端渲染指南 中说明的权衡取舍,可以用 fetchQuery 替代 prefetch。这样既能服务端使用数据,又能将其水合(hydrate)至客户端:

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>
);
}