使用 @trpc/react-query:在 React 应用中搭建端到端类型安全的 tRPC 数据层
本篇技术指南围绕 tRPC 官方 React 集成包 @trpc/react-query 展开,讲解如何在 React 项目中把 tRPC 的端到端类型安全过程调用与 TanStack Query(react-query)的缓存、失效与状态管理能力结合起来。你会依次掌握包的安装方式、从创建类型化 hooks 到接入 Provider、再到在组件中发起 useQuery 调用的三步走用法,并通过阅读仓库源码理解代理解码、查询键生成、请求派发链路以及 useUtils 等命令式工具的设计原理。读完即可在自己的 React / Next.js 全栈项目里落地一套可复制、可运行的"零手工 API 层"实践。
一、@trpc/react-query 是什么
@trpc/react-query 在 tRPC 仓库中定义为一个 "tRPC wrapper around react-query",即"基于 react-query 的 tRPC React 封装"(见 packages/react-query/README.md)。它要解决的核心问题是:让 React 组件直接用 trpc.xxx.useQuery() 这种符合直觉的调用形式去发起远程过程调用,同时把 tRPC 的类型信息完整地透传到 TanStack Query 的 Hooks 之上,让输入、输出、错误类型在编译期全程可推导。
需要说明的是,tRPC v11 在保留本包的同时,还演进出了另一条以 @trpc/tanstack-react-query 为代表的"新一代"集成路线(其技能文档见 packages/tanstack-react-query/skills)。@trpc/react-query 则是文档中沿用了多个大版本的"经典"封装形态,其 API 以 createTRPCReact 为中心,本文所讲内容与仓库版本 11.18.0 保持一致(见 packages/react-query/package.json)。
从依赖关系看,本包将以下包声明为 peer dependency:
| 依赖 | 版本要求 | 作用 |
|---|---|---|
@tanstack/react-query |
^5.80.3 |
提供 QueryClient、useQuery 等底层能力 |
@trpc/client |
11.18.0 |
提供 tRPC 客户端与 HTTP 传输 |
@trpc/server |
11.18.0 |
提供类型层面的 Router / Procedure 定义 |
react |
>=18.2.0 |
React 运行时 |
typescript |
>=5.7.2 |
提供类型推导所需编译能力 |
因此,@trpc/react-query 本身不重复实现数据缓存或请求传输,它是把 tRPC 的 Router 类型与 TanStack Query v5 的数据获取语义"焊"在一起的类型层和胶水层。
二、安装
在项目根目录执行任一包管理器命令即可(四个包管理器命令均为仓库 README 原始内容):
# npm
npm install @trpc/react-query @tanstack/react-query
# Yarn
yarn add @trpc/react-query @tanstack/react-query
# pnpm
pnpm add @trpc/react-query @tanstack/react-query
# Bun
bun add @trpc/react-query @tanstack/react-query
安装时请把 @tanstack/react-query 一并显式安装,因为它作为 peer dependency 不会被自动带入。包同时提供多种子路径导出:@trpc/react-query(主入口)、@trpc/react-query/rsc(React Server Components 支持,对应源码 packages/react-query/src/rsc.tsx)、@trpc/react-query/server 与 @trpc/react-query/shared(见 packages/react-query/package.json 中的 exports 字段)。
三、基础示例:三步把类型安全接到 React 组件
第一步:创建导出 hooks 的 utils 文件
与 tRPC 惯例一致,先创建一个工具文件,用服务端导出的 AppRouter 类型去实例化一个全局单例:
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from './server';
export const trpc = createTRPCReact<AppRouter>();
这里的 createTRPCReact 正是本包最核心的入口(实现见 packages/react-query/src/createTRPCReact.tsx)。它返回的并不是一个普通对象,而是一个"扁平代理"(flat proxy):你对 trpc.greeting、trpc.user.list 这类路径的属性访问会被代理拦截并记录为路径数组,最终路由到对应 Procedure 的 Hooks 上。换句话说,只要 AppRouter 中注册了某个 query/mutation/subscription,代理上就会自动出现对应类型的调用入口,无需手写任何客户端定义。
从源码结构看,createTRPCReact 内部实际做了两层组合(见 packages/react-query/src/createTRPCReact.tsx#L508-L518):
createRootHooks(opts)创建包含Provider、useQuery、useMutation等真实实现的一组 hooks(见 packages/react-query/src/shared/hooks/createHooksInternal.tsx);createHooksInternal再用createFlatProxy包一层,把useUtils/useContext、Provider、createClient等固定成员与根据 Router 类型生成的 procedure 装饰对象合并到一个统一的代理对象上(见 packages/react-query/src/createTRPCReact.tsx#L477-L506)。
第二步:用 Provider 把客户端与 QueryClient 注入应用
在应用根部,通过 trpc.createClient 创建 tRPC 客户端,并把它与一个 TanStack Query 的 QueryClient 一起挂在 trpc.Provider 下:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { trpc } from '~/utils/trpc';
import React, { useState } from 'react';
export function App() {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
trpc.createClient({
url: 'http://localhost:5000/trpc',
}),
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{/* Your app here */}
</QueryClientProvider>
</trpc.Provider>
);
}
几个值得注意的工程细节:
- 为什么要用
useState初始化:new QueryClient()和trpc.createClient()都应该只创建一次,避免每次渲染都重建导致缓存与连接状态丢失;useState的惰性初始化正好满足这一需求。 - Provider 的职责:
TRPCProvider会把client、queryClient、ssrContext、ssrState、abortOnUnmount等组装成一个 context 值下发给所有后代组件(结构定义见 packages/react-query/src/internals/context.tsx#L73-L132,Provider 实现见 packages/react-query/src/shared/hooks/createHooksInternal.tsx#L100-L140)。 - 为什么还要包一层
QueryClientProvider:@trpc/react-query的 hooks 最终仍会委托给@tanstack/react-query的useQuery/useMutation等 hooks,因此组件树中必须存在 TanStack Query 自己的 Provider。这个示例还演示了在开启 SSR 时如何用useState生成带自定义defaultOptions的 QueryClient(仓库中 examples/next-sse-chat/src/lib/query-client.ts 演示了为 SSR 设置staleTime的做法)。
若你的项目服务端基于 standalone/fetch 适配器运行(例如 localhost:5000/trpc 上的 tRPC HTTP 端点),客户端传输层链接配置还可以传入 links 数组来自定义,这里用最简的 url 方式即可启动。
第三步:在组件里发起类型安全的查询
完成注入后,任何组件都能用 useQuery 拿到后端 query 的数据与状态:
import { trpc } from '~/utils/trpc';
export function Hello() {
const { data, error, status } = trpc.greeting.useQuery({ name: 'tRPC' });
if (error) {
return <p>{error.message}</p>;
}
if (status !== 'success') {
return <p>Loading...</p>;
}
return <div>{data && <p>{data.greeting}</p>}</div>;
}
这里 trpc.greeting 的参数对象 { name: 'tRPC' } 的类型与后端过程声明的输入校验 schema 完全一致,data.greeting 的类型也直接来自后端输出类型——全链路无手工接口契约。data / error / status 取自 TanStack Query v5 的结果形态,因此你熟悉的 isLoading、isError、refetch 等能力全部可用;同时 hooks 返回结果还被额外装饰了 trpc.path 等元信息(源码中通过 useHookResult 实现,见 packages/react-query/src/shared/hooks/createHooksInternal.tsx 中 hook.trpc = useHookResult(...) 的赋值)。
四、深入源码:Procedure 是如何被"装饰"成 Hooks 的
类型安全不是魔法,而是建立在 TypeScript 条件类型 + 运行时代理两层机制之上。
运行时侧:以 createTRPCReact 为例,任何对代理的链式访问最终都会落到 createReactDecoration 生成的递归代理上(见 packages/react-query/src/shared/proxy/decorationProxy.ts):
return createRecursiveProxy(({ path, args }) => {
const pathCopy = [...path];
const lastArg = pathCopy.pop()!; // 例如 'useQuery' / 'useMutation'
if (lastArg === 'useMutation') {
return hookslastArg;
}
if (lastArg === '_def') {
return { path: pathCopy }; // 供 getQueryKey 等工具取路径
}
const [input, ...rest] = args;
return hookslastArg;
});
也就是说,trpc.user.list.useQuery({...}) 会被规约成对 useQuery(['user','list'], input, opts) 的调用,路径中的每一段(包括嵌套 router)都被保留下来,用于之后拼装查询键。
类型侧:DecorateProcedure 依据 Procedure 的 type 三态分发(见 packages/react-query/src/createTRPCReact.tsx#L403-L441):
query→DecoratedQuery,拥有useQuery、usePrefetchQuery、useSuspenseQuery,以及当输入含cursor时的useInfiniteQuery系列;mutation→DecoratedMutation,拥有useMutation;subscription→ 拥有useSubscription,其数据类型基于输出异步迭代器的 yield 类型推导。
DecoratedQueryMethods(见 packages/react-query/src/createTRPCReact.tsx#L322-L345)为每个 query 方法同时提供普通结果与元数据 TRPCHookResult,并把 input/output/errorShape/transformer 从 Procedure 定义中逐个提取出来,保证每个 hooks 的泛型签名都是闭合且精确的。
五、查询键与请求链路:从 React 状态到 HTTP 调用
Hooks 拿到"路径数组 + input"之后,如何变成一次真实的 tRPC 请求?关键在两步:
1. 生成查询键(Query Key)。getQueryKeyInternal(见 packages/react-query/src/internals/getQueryKey.ts)把 tRPC 过程路径结构化为 TanStack Query 可缓存的结构化键:
export type TRPCQueryKey = [
readonly string[], // 例如 ['user','list']
{ input?: unknown; type?: 'infinite' | 'query' }?,
];
设计上刻意把 input 独立成第二个数组元素并标记 type,从而支持对"同一个 router 下所有查询"做整体失效——当没有 input 且 type 为 any 时键会被压缩成 [path],这正对应 utils.invalidate() 想命中整棵子树的场景。此外,无限查询会从 input 中剔除保留字段 cursor 与 direction 再入键,避免翻页参数污染缓存键。包从主入口还导出了供你在应用层显式使用的 getQueryKey / getMutationKey(见 packages/react-query/src/index.ts),mutation 的键形如 [path]。
2. 派发请求。真正的网络调用发生在 react-query 提供的 queryFn 内部(见 packages/react-query/src/shared/hooks/createHooksInternal.tsx 的 useQuery 实现):通过 getClientArgs 把 TRPCQueryKey 还原为 [path.join('.'), input, opts.trpc] 元组(见 packages/react-query/src/internals/getClientArgs.ts),再调用底层 tRPC 客户端(@trpc/client 的 createTRPCClient,即 Provider 上暴露的 createClient)。opts.trpc 这一命名空间承载了 ssr、abortOnUnmount、signal 等 tRPC 专有选项,与 TanStack Query 自身的选项互不干扰。
六、Hooks 全景:查询、变更、订阅与无限查询
基于仓库实现,@trpc/react-query 的装饰层按 Procedure 类型提供如下 Hooks(全部位于 packages/react-query/src/shared/hooks/createHooksInternal.tsx):
| Hooks | 适用 Procedure | 关键行为与扩展点 |
|---|---|---|
useQuery(input?, opts?) |
query | 委托 @tanstack/react-query 的 useQuery;支持 skipToken 作 input 以禁用请求;输出为异步迭代器时会被转成可缓存结果 |
useSuspenseQuery |
query | 基于 React Suspense 的形态,返回 [data, result] 元组,供 <Suspense> 场景使用 |
usePrefetchQuery |
query | 借助 TanStack v5 的 usePrefetchQuery 在渲染期间预取,不触发额外状态 |
useInfiniteQuery |
含 cursor 输入的 query |
自动把 react-query 的 pageParam/direction 映射为 tRPC 输入的 cursor/direction 保留字段;用 opts.initialCursor 指定首页参数 |
useSuspenseInfiniteQuery / usePrefetchInfiniteQuery |
同上 | Suspense 与预取变体 |
useMutation(opts?) |
mutation | 委托 useMutation,mutationFn 内部走 client.mutation;支持通过 config.overrides.useMutation.onSuccess 全局改写成功回调 |
useSubscription(input?, opts?) |
subscription | 直接订阅 tRPC 底层流(client.subscription),结果状态机含 idle/connecting/pending/error;依赖 enabled 与 skipToken 控制开关,卸载时自动退订 |
useQueries(fn) / useSuspenseQueries(fn) |
query 集合 | 用回调 + 类型化代理同时声明多个查询(实现见 packages/react-query/src/internals/useQueries.ts 及 packages/react-query/src/shared/proxy/useQueriesProxy.ts) |
其中有两个设计细节值得展开:
- SSR 预取:当运行在服务端且
ssrState === 'prepass'、opts.trpc.ssr !== false、enabled不为false且缓存中无该键时,useQuery/useInfiniteQuery/useQueries会自动触发prefetchQuery,把数据在渲染前填充进 QueryClient。这也是与 Next.js 等框架配合实现"首屏即数据"的基础。 - abortOnUnmount:组件卸载时是否中止进行中的请求,可通过
opts.trpc.abortOnUnmount、createTRPCReact全局配置或 Provider 的abortOnUnmountprop 三级控制;启用时会把 react-query 的AbortSignal透传给 tRPC 客户端(相关代码在createHooksInternal.tsx中多次出现的shouldAbortOnUnmount逻辑处)。Provider 上也提供了ssrContext(SSR 请求上下文,如req/res)与ssrState(取值为'mounted' | 'mounting' | 'prepass' | false)两个选项,类型定义与默认值见 packages/react-query/src/internals/context.tsx#L70-L98。
七、useUtils:脱离组件也能管理缓存
类组件或事件处理器里需要主动刷新数据时,可以调用 trpc.useUtils()(useContext 是其已弃用别名,二者类型定义见 packages/react-query/src/createTRPCReact.tsx#L446-L461)。它返回的上下文同时携带了 Provider 注入的全部能力,以及按 Router 形状类型化生成的工具集合,常用写法例如"mutation 成功后让相关查询失效":
const utils = trpc.useUtils();
const mutation = trpc.post.create.useMutation({
onSuccess: () => {
// 让 post 路由下所有查询缓存失效并自动重新拉取
utils.post.invalidate();
},
});
这些命令式方法在 packages/react-query/src/utils/createUtilityFunctions.ts 中逐一实现并直接委托给 QueryClient:
- 预取/读取:
prefetchQuery、prefetchInfiniteQuery、ensureQueryData、fetchQuery、fetchInfiniteQuery、getQueryData、getInfiniteQueryData; - 失效与刷新:
invalidateQueries、refetchQueries、resetQueries、cancelQuery; - 就地写入:
setQueryData、setQueriesData、setInfiniteQueryData; - 类型安全的选项工厂:
queryOptions、infiniteQueryOptions(自动装配queryKey与queryFn); - mutation 默认值管理:
setMutationDefaults、getMutationDefaults、isMutating。
需要注意,像 invalidate 这种挂在 utils.<routerPath> 下的链式入口由另一层类型化代理 utilsProxy 提供(见 packages/react-query/src/shared/proxy/utilsProxy.ts),它会按 [path] 前缀精确命中整个子树。
如果你需要在组件之外(例如路由层或事件处理中)创建带类型的工具对象,包还导出了 createTRPCQueryUtils(见 packages/react-query/src/createTRPCQueryUtils.tsx),传入 { client, queryClient } 即可获得与 useUtils() 相同能力但与 React 生命周期解耦的实例。
八、服务端调用、RSC 与真实示例
@trpc/react-query 在 SSR 与 Server Components 场景下需要额外的类型化调用手段:包通过子路径导出 ./server 与 ./rsc,其中服务端侧实现(含基于代理的 ssgProxy)位于 packages/react-query/src/server,RSC 入口为 packages/react-query/src/rsc.tsx,适用于在服务端组件中直接发起"脱水"查询、避免把敏感数据下发给浏览器。Next.js App Router / Pages Router 的完整接入方式在仓库 www/docs/client/react 与 packages/next/skills 目录下有配套说明。
仓库中还提供了真实的端到端范例:例如 examples/next-sse-chat/src/lib/trpc.ts 中的 export const trpc = createTRPCReact<AppRouter>() 与 examples/next-sse-chat/src/lib/query-client.ts 中针对 SSR 定制 staleTime 的 createQueryClient,它们与本文的三步走骨架完全同构,可直接作为参照;后续再搭配 packages/tanstack-react-query/skills/react-query-classic-migration 的迁移说明,即可在理解经典封装的基础上平滑演进到新一代 @trpc/tanstack-react-query 集成。
小结
一句话总结这套模式的收益:Router 定义即唯一契约。@trpc/react-query 通过 createTRPCReact<AppRouter>() 把服务端过程注册表转化为组件可直接消费的类型化 hooks,再把 react-query 的缓存、失效、Suspense、预取语义完整保留下来,让"写后端"与"调接口"之间不再需要任何手写桥接层。对 React 技术栈的读者而言,建议从第三步组件示例入手跑通最小链路,再逐步深入第六、七节的缓存管理与 SSR 细节,即可在真实项目中稳定落地。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00