与 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
- 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. 创建 tRPC 路由
在 trpc/init.ts 中使用 initTRPC 函数初始化 tRPC 后端,并创建您的第一个路由。我们将在此创建简单的 "hello world" 路由和过程 - 如需深入了解如何创建 tRPC API,请参考 tRPC 的 快速入门指南 和 后端使用文档。
此处使用的文件名并非 tRPC 强制要求,您可采用任意文件结构。
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. 创建 Query Client 工厂函数
创建共享文件 trpc/query-client.ts,导出用于创建 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,},},});}
我们在此设置若干默认选项:
-
staleTime:在 SSR 场景下,通常需设置大于 0 的默认 staleTime 以避免客户端立即重新获取数据 -
shouldDehydrateQuery:此函数决定查询是否应被脱水。由于 RSC 传输协议支持通过网络水合 promise,我们扩展了defaultShouldDehydrateQuery函数以包含仍在挂起的查询。这将允许我们在树形结构顶层的服务端组件启动预获取,并在下层的客户端组件消费该 promise -
serializeData和deserializeData(可选):若您在前一步设置了 数据转换器,请配置此选项以确保在跨越服务端-客户端边界水合 query client 时正确序列化数据
4. 为客户端组件创建 tRPC 客户端
trpc/client.tsx 是从客户端组件使用 tRPC API 的入口点。在此文件中导入 tRPC 路由器的类型定义,并使用 createTRPCReact 创建类型安全的钩子。我们还将从此文件导出上下文提供者。
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>);}
将此提供者挂载到应用的根节点(例如使用 Next.js 时挂载到 app/layout.tsx)。
5. 为服务端组件创建 tRPC 调用器
要在服务器组件中预取查询,我们使用 tRPC 调用器。@trpc/react-query/rsc 模块导出了 createCaller 的轻量封装器,可与您的 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,);
API 使用实践
现在你可以在应用中使用 tRPC API。虽然你可以在客户端组件中像其他 React 应用那样使用 React Query 钩子,
但我们还能利用 RSC 的特性:在高层级服务端组件中预取查询。你可能熟悉这种被称为"随取随渲"(render as you fetch)的模式,
它通常通过加载器实现。这意味着请求会尽早触发,但不会阻塞渲染,直到实际需要使用数据时才通过 useQuery 或 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>;}
利用 Suspense
你可以使用 useSuspenseQuery 钩子,通过 Suspense 和 Error Boundaries 来处理加载与错误状态。
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>;}
在服务端组件获取数据
如果需要在服务端组件访问数据,可以直接调用过程(procedure)而无需使用 .prefetch(),就像使用常规服务端调用那样。请注意此方法与查询客户端是分离的,不会将数据存储在缓存中。这意味着无法在服务端组件使用数据并期望它在客户端可用,这是有意为之的设计,在高级服务端渲染指南中有详细说明。
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>;}