跳至主内容
版本:11.x

与 React 服务器组件的设置

非官方测试版翻译

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

技巧

本文档介绍的是我们"经典版"的 React Query 集成方案(虽然仍受支持),但不建议新 tRPC 项目使用。我们推荐改用新的 TanStack React Query 集成方案

本指南概述了如何在 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/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 路由器的类型定义,并使用 createTRPCReact 创建类型安全的钩子。我们还将从此文件导出上下文提供者。

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 { 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 client
return makeQueryClient();
}
// Browser: use singleton pattern to keep the same query client
return (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 boundary
const queryClient = getQueryClient();
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
// transformer: superjson, <-- if you use a data transformer
url: getUrl(),
}),
],
}),
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{props.children}
</QueryClientProvider>
</trpc.Provider>
);
}
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 { 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 client
return makeQueryClient();
}
// Browser: use singleton pattern to keep the same query client
return (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 boundary
const queryClient = getQueryClient();
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
// transformer: superjson, <-- if you use a data transformer
url: getUrl(),
}),
],
}),
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{props.children}
</QueryClientProvider>
</trpc.Provider>
);
}

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

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

要在服务器组件中预取查询,我们使用 tRPC 调用器。@trpc/react-query/rsc 模块导出了 createCaller 的轻量封装器,可与您的 React Query 客户端集成。

trpc/server.tsx
tsx
import 'server-only'; // <-- ensure this file cannot be imported from the client
import { 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.tsx
tsx
import 'server-only'; // <-- ensure this file cannot be imported from the client
import { 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,
);

API 使用实践

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

app/page.tsx
tsx
import { 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.tsx
tsx
import { 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.tsx
tsx
'use client';
// <-- hooks can only be used in client components
import { 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.tsx
tsx
'use client';
// <-- hooks can only be used in client components
import { 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>;
}

利用 Suspense

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

app/page.tsx
tsx
import { 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.tsx
tsx
import { 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.tsx
tsx
'use client';
import { trpc } from '~/trpc/client';
export function ClientGreeting() {
const [data] = trpc.hello.useSuspenseQuery();
return <div>{data.greeting}</div>;
}
app/client-greeting.tsx
tsx
'use client';
import { trpc } from '~/trpc/client';
export function ClientGreeting() {
const [data] = trpc.hello.useSuspenseQuery();
return <div>{data.greeting}</div>;
}

在服务端组件获取数据

如果需要在服务端组件访问数据,可以直接调用过程(procedure)而无需使用 .prefetch(),就像使用常规服务端调用那样。请注意此方法与查询客户端是分离的,不会将数据存储在缓存中。这意味着无法在服务端组件使用数据并期望它在客户端可用,这是有意为之的设计,在高级服务端渲染指南中有详细说明。

app/page.tsx
tsx
import { 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.tsx
tsx
import { 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>;
}