与 React 服务器组件的设置
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
本文档介绍的是我们"经典版"的 React Query 集成方案(虽然仍受支持),但不建议新 tRPC 项目使用。我们推荐改用新的 TanStack React Query 集成方案。
使用 Next.js? 请查阅专门的 Next.js App Router 设置指南 获取推荐方案。
本指南概述了如何在 React Server Components (RSC) 框架(如 Next.js App Router)中使用 tRPC。需要注意的是,RSC 本身已解决了 tRPC 设计初衷要解决的许多问题,因此您可能完全不需要 tRPC。
tRPC 与 RSC 的集成并无万能方案,因此本指南仅作为起点,请根据您的具体需求和偏好进行调整。
若需了解如何在 Server Actions 中使用 tRPC,请参阅 Server Actions 指南。
请先阅读 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" 路由器和过程(procedure)——若需深入了解如何创建 tRPC API,请查阅 tRPC 的 快速入门指南 和 后端使用文档。
此处使用的文件名并非 tRPC 强制要求,您可采用任意文件结构。
View sample backend
trpc/init.tstsimport {initTRPC } from '@trpc/server';import {cache } from 'react';export constcreateTRPCContext =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.constt =initTRPC .create ({/*** @see https://trpc.io/docs/server/data-transformers*/// transformer: superjson,});// Base router and procedure helpersexport constcreateTRPCRouter =t .router ;export constcreateCallerFactory =t .createCallerFactory ;export constbaseProcedure =t .procedure ;
trpc/init.tstsimport {initTRPC } from '@trpc/server';import {cache } from 'react';export constcreateTRPCContext =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.constt =initTRPC .create ({/*** @see https://trpc.io/docs/server/data-transformers*/// transformer: superjson,});// Base router and procedure helpersexport constcreateTRPCRouter =t .router ;export constcreateCallerFactory =t .createCallerFactory ;export constbaseProcedure =t .procedure ;
trpc/routers/_app.tstsimport {z } from 'zod';import {baseProcedure ,createTRPCRouter } from '../init';export constappRouter =createTRPCRouter ({hello :baseProcedure .input (z .object ({text :z .string (),}),).query ((opts ) => {return {greeting : `hello ${opts .input .text }`,};}),});// export type definition of APIexport typeAppRouter = typeofappRouter ;
trpc/routers/_app.tstsimport {z } from 'zod';import {baseProcedure ,createTRPCRouter } from '../init';export constappRouter =createTRPCRouter ({hello :baseProcedure .input (z .object ({text :z .string (),}),).query ((opts ) => {return {greeting : `hello ${opts .input .text }`,};}),});// export type definition of APIexport typeAppRouter = typeofappRouter ;
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';consthandler = (req :Request ) =>fetchRequestHandler ({endpoint : '/api/trpc',req ,router :appRouter ,createContext :createTRPCContext ,});export {handler asGET ,handler asPOST };
app/api/trpc/[trpc]/route.tstsimport {fetchRequestHandler } from '@trpc/server/adapters/fetch';import {createTRPCContext } from '../../../../trpc/init';import {appRouter } from '../../../../trpc/routers/_app';consthandler = (req :Request ) =>fetchRequestHandler ({endpoint : '/api/trpc',req ,router :appRouter ,createContext :createTRPCContext ,});export {handler asGET ,handler asPOST };
3. 创建 Query Client 工厂函数
创建共享文件 trpc/query-client.ts,导出用于创建 QueryClient 实例的函数。
trpc/query-client.tstsimport {defaultShouldDehydrateQuery ,QueryClient ,} from '@tanstack/react-query';importsuperjson from 'superjson';export functionmakeQueryClient () {return newQueryClient ({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';importsuperjson from 'superjson';export functionmakeQueryClient () {return newQueryClient ({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';importReact , {useState } from 'react';import {makeQueryClient } from './query-client';import type {AppRouter } from './routers/_app';export consttrpc =createTRPCReact <AppRouter >();letclientQueryClientSingleton :QueryClient ;functiongetQueryClient () {if (typeofwindow === 'undefined') {// Server: always make a new query clientreturnmakeQueryClient ();}// Browser: use singleton pattern to keep the same query clientreturn (clientQueryClientSingleton ??=makeQueryClient ());}functiongetUrl () {constbase = (() => {if (typeofwindow !== 'undefined') return '';if (process .env .VERCEL_URL ) return `https://${process .env .VERCEL_URL }`;return 'http://localhost:3000';})();return `${base }/api/trpc`;}export functionTRPCProvider (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 boundaryconstqueryClient =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';importReact , {useState } from 'react';import {makeQueryClient } from './query-client';import type {AppRouter } from './routers/_app';export consttrpc =createTRPCReact <AppRouter >();letclientQueryClientSingleton :QueryClient ;functiongetQueryClient () {if (typeofwindow === 'undefined') {// Server: always make a new query clientreturnmakeQueryClient ();}// Browser: use singleton pattern to keep the same query clientreturn (clientQueryClientSingleton ??=makeQueryClient ());}functiongetUrl () {constbase = (() => {if (typeofwindow !== 'undefined') return '';if (process .env .VERCEL_URL ) return `https://${process .env .VERCEL_URL }`;return 'http://localhost:3000';})();return `${base }/api/trpc`;}export functionTRPCProvider (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 boundaryconstqueryClient =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 constgetQueryClient =cache (makeQueryClient );constcaller =createCallerFactory (appRouter )(createTRPCContext );export const {trpc ,HydrateClient } =createHydrationHelpers <typeofappRouter >(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 constgetQueryClient =cache (makeQueryClient );constcaller =createCallerFactory (appRouter )(createTRPCContext );export const {trpc ,HydrateClient } =createHydrationHelpers <typeofappRouter >(caller ,getQueryClient ,);
API 使用实践
现在你可以在应用中使用 tRPC API。虽然你可以在客户端组件中像其他 React 应用那样使用 React Query 钩子,
但我们还能利用 RSC 的特性:在高层级服务端组件中预取查询。你可能熟悉这种被称为"随取随渲"(render as you fetch)的模式,
它通常通过加载器实现。这意味着请求会尽早触发,但不会阻塞渲染,直到实际需要使用数据时才通过 useQuery 或 useSuspenseQuery 钩子挂起。
app/page.tsxtsximport {trpc ,HydrateClient } from '../trpc/server';import {ClientGreeting } from './client-greeting';export default async functionHome () {voidtrpc .hello .prefetch ();return (<HydrateClient ><div >...</div >{/** ... */}<ClientGreeting /></HydrateClient >);}
app/page.tsxtsximport {trpc ,HydrateClient } from '../trpc/server';import {ClientGreeting } from './client-greeting';export default async functionHome () {voidtrpc .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 functionClientGreeting () {constgreeting =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 functionClientGreeting () {constgreeting =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 ,HydrateClient } from '../trpc/server';import {Suspense } from 'react';import {ErrorBoundary } from 'react-error-boundary';import {ClientGreeting } from './client-greeting';export default async functionHome () {voidtrpc .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 ,HydrateClient } from '../trpc/server';import {Suspense } from 'react';import {ErrorBoundary } from 'react-error-boundary';import {ClientGreeting } from './client-greeting';export default async functionHome () {voidtrpc .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 functionClientGreeting () {const [data ] =trpc .hello .useSuspenseQuery ();return <div >{data .greeting }</div >;}
app/client-greeting.tsxtsx'use client';import {trpc } from '../trpc/client';export functionClientGreeting () {const [data ] =trpc .hello .useSuspenseQuery ();return <div >{data .greeting }</div >;}
在服务端组件获取数据
若需在服务端组件中访问数据,可直接调用过程(procedure)而非使用 .prefetch(),正如使用常规服务端调用器的方式。请注意,此方法与查询客户端分离且不会将数据存入缓存。这意味着您无法在服务端组件使用数据后期望其在客户端可用,此设计有意为之,详情请参阅高级服务端渲染指南。
app/page.tsxtsximport {trpc } from '../trpc/server';export default async functionHome () {// Use the caller directly without using `.prefetch()`constgreeting = awaittrpc .hello ();// ^? { greeting: string }return <div >{greeting .greeting }</div >;}
app/page.tsxtsximport {trpc } from '../trpc/server';export default async functionHome () {// Use the caller directly without using `.prefetch()`constgreeting = awaittrpc .hello ();// ^? { greeting: string }return <div >{greeting .greeting }</div >;}