tRPC 组件外使用查询工具:用 createTRPCQueryUtils 接管 react-router loader 等场景的缓存管理 tRPC 组件外使用查询工具用 createTRPCQueryUtils 接管 react-router loader 等场景的缓存管理【免费下载链接】trpc‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc本篇指南讲解trpc/react-query提供的createTRPCQueryUtils它让你在 React 组件之外例如 react-router 的 route loader、服务端 SSR 预取或任意非 Hook 环境也能以类型安全的方式管理由trpc/react-query发起查询的缓存数据。读完本文你将掌握createTRPCQueryUtils与useUtils的差异与选型原则、完整的 loader 组件数据接力写法、全部 helpers 的语义含queryOptions/infiniteQueryOptions并从仓库源码与测试用例层面理解其薄封装tanstack/react-queryQueryClient的实现原理。为什么需要 createTRPCQueryUtils在 tRPC v11 的 React 集成中缓存查询数据的常规入口是trpc.useUtils()旧版本名为useContext()见 useUtils 文档。它本质上是一个 React Hook底层依赖useQueryClient因此只有在 React 组件的渲染上下文中才能被调用。但现实项目中存在大量非组件调用点react-router / Remix / TanStack Router 的route loader在组件渲染前执行数据预取事件回调、定时任务、工具函数等脱离组件树但仍需读写缓存的代码希望共享同一份QueryClient让预取的数据直接喂给随后挂载的组件。这正是createTRPCQueryUtils的用途把查询工具从 Hook 的束缚中解放出来——你只需要在创建时显式传入queryClient与client两个对象之后就能在任何地方使用与useUtils完全同构的 helpers。官方文档的警告见 createTRPCQueryUtils.md请避免在 React 组件内使用createTRPCQueryUtils组件内应始终使用useUtils——后者是真正的 Hook内部用useCallback与useQueryClient做了正确封装能更好地融入 React 渲染模型。useUtils 与 createTRPCQueryUtils 的选型对照维度useUtils()HookcreateTRPCQueryUtils()普通函数调用位置React 组件内部任意位置route loader、SSR、普通 TS 模块底层依赖useQueryClientuseCallback手动传入的QueryClientQueryClient 来源由QueryClientProvider上下文注入调用方显式构造并传入客户端client来源Provider 上下文调用方显式传入创建时的client返回的 helpers与 tRPC client 同构的代理对象完全一致两者的返回结构完全一致都是一个以 router 树为路径、末端挂满 helpers的类型安全代理对象。因此凡是useUtils支持的操作fetch、prefetch、ensureData、invalidate、queryOptions等createTRPCQueryUtils都同样支持唯一区别只是对象从哪来、在哪儿能用。使用示例在 react-router loader 中预取数据假设服务端定义了post子路由内含all查询此处为文档与源码测试中反复使用的典型结构见 server.ts 片段 与 createQueryUtils.test.ts 的测试路由import { initTRPC } from trpc/server; const t initTRPC.create(); const appRouter t.router({ post: t.router({ all: t.procedure.query(() { return { posts: [ { id: 1, title: everlong }, { id: 2, title: After Dark }, ], }; }), }), }); export type AppRouter typeof appRouter;现在在 react-router 风格的页面中先用createTRPCQueryUtils构造一个无 Hook的工具对象并在 loader 里调用post.all.ensureData()import { QueryClient } from tanstack/react-query; import { createTRPCQueryUtils, createTRPCReact } from trpc/react-query; import type { AppRouter } from ./server; const trpc createTRPCReactAppRouter(); const trpcClient trpc.createClient({ links: [] }); const queryClient new QueryClient(); const clientUtils createTRPCQueryUtils({ queryClient, client: trpcClient }); // This is a react-router loader export async function loader() { // Fetches data if it doesnt exist in the cache const allPostsData await clientUtils.post.all.ensureData(); return { allPostsData, }; } // This is a react component export function Component() { const loaderData useLoaderData() as AwaitedReturnTypetypeof loader; const allPostQuery trpc.post.all.useQuery(undefined, { initialData: loaderData.allPostsData, // Uses the data from the loader }); return ( div {allPostQuery.data.posts.map((post) ( div key{post.id}{post.title}/div ))} /div ); }这个例子揭示了最核心的工程模式loader 中写入缓存 → 组件用initialData读取缓存。ensureData()的行为是命中缓存直接返回、未命中才发起请求因此 loader 与组件共享同一个queryClient时既不会重复请求又能让首屏渲染立即拥有数据。SSR / Remix 场景必须注意每请求新建 QueryClient文档专门给出了一条注意事项如果你正在使用 Remix Run 或自己做 SSR不能为每个请求复用同一个queryClient。因为不同用户/不同请求之间会共享缓存造成数据串扰cross-request data leakage。正确做法是为每个请求创建新的queryClient配合 SSR 预取把数据以 dehydrated 形式随页面下发。这也是createTRPCQueryUtils把queryClient作为构造参数显式传入的根本原因——它不强绑定全局单例你可以在每个请求的生命周期内自行组装并释放。直接访问 client与useUtils一样当你在组件外确实需要直接发起过程调用如 mutation时无需再单独创建一个 vanilla client你可以直接使用当初传给createTRPCQueryUtils的client对象。例如文档提及的通过utils.client.apiKey.create.mutate()获取返回值的写法在非组件环境中同样成立只是不再需要经trpc.useUtils()解包。源码级原理从 createTRPCQueryUtils 到 QueryClient要彻底掌握它值得沿着仓库源码走一遍调用链。函数本体极其精简位于 createTRPCQueryUtils.tsxexport function createTRPCQueryUtilsTRouter extends AnyRouter( opts: CreateQueryUtilsOptionsTRouter, ) { const utils createUtilityFunctions(opts); return createQueryUtilsProxyTRouter(utils); }它只做两件事createUtilityFunctions(opts)把配置对象{ queryClient, client }转成一组薄封装函数。源码见 createUtilityFunctions.ts其函数签名明确要求clientTRPCClient或TRPCUntypedClient与queryClient来自tanstack/react-query两个字段。在实现内部它会先借助getUntypedClient(client)取得底层 untyped client——这一点从源码可以看出所有 helpers 最终统一通过untypedClient.query(...)/untypedClient.mutation(...)走getClientArgs()生成的参数调用远程过程而真正的缓存状态则完全交给QueryClient的对应方法。createQueryUtilsProxy(utils)用递归 Proxy 把上述扁平函数集合还原成与你的 router 结构一一对应的树形对象。见 utilsProxy.ts 中的 createRecursiveUtilsProxy每次访问形如clientUtils.post.all.fetch的属性链时Proxy 会把属性路径收集为path把最后一个属性视为要调用的工具名utilName随后通过getQueryType()判断它是普通 query、infinite query 还是 mutation 相关操作再用getQueryKeyInternal()拼出标准 query key最后分派给createUtilityFunctions产出的底层函数。const queryType getQueryType(utilName); const queryKey getQueryKeyInternal(path, input, queryType); // e.g. 输入 [post,all] 与 input得到形如 [[post,all], {...input}] 的 queryKey由此可见类型安全、tree-shakable、行为可预测的根源并不神秘Proxy 负责路由映射QueryClient负责缓存状态untyped client 负责网络 I/O。从代码结构上可以推断正因为全部能力收敛在QueryClient之上createTRPCQueryUtils创建的实例与useUtils返回的实例对同一QueryClient的操作是完全互通、互相可见的——这正是 loader 写入、组件读取能无缝衔接的原因。Helpers 全览与底层映射通过createTRPCQueryUtils可以访问的 helpers 与useUtils完全一致这也是文档明示只需传入queryClient和client即可的原因。下表汇总了 tRPC helper 与tanstack/react-query方法之间的对应关系完整对照表见 useUtils 文档的 Helpers 一节tRPC helper底层 QueryClient 方法典型用途fetch/fetchInfinitefetchQuery/fetchInfiniteQuery主动发起一次请求并把结果写入缓存prefetch/prefetchInfiniteprefetchQuery/prefetchInfiniteQuery预热缓存不抛出错误ensureDataensureQueryData有缓存即返回无缓存才请求loader 首选invalidateinvalidateQueries使缓存失效并触发重取refetchrefetchQueries强制重取cancelcancelQueries取消进行中的请求resetresetQueries将缓存重置为初始数据setData/getDatasetQueryData/getQueryData直接写/读单条查询缓存setQueriesDatasetQueriesData按过滤器批量写缓存setInfiniteData/getInfiniteDatasetQueryData/getQueryDatainfinite 形态读写无限列表缓存setMutationDefaults/getMutationDefaultssetMutationDefaults/getMutationDefaults为 mutation 预置默认行为isMutatingisMutating查询当前进行中的 mutation 数量此外createTRPCQueryUtils返回的对象还支持queryOptions与infiniteQueryOptions两个选项构造器。在 createUtilityFunctions.ts 中可以看到它们的实现这些函数接收 router 路径与 query key自动生成带queryFn的 options 对象供useQuery/useSuspenseQuery/useInfiniteQuery等消费。正因为 loader 与组件可共享同一份 options 构造结果你可以在组件外预取数据再把完全一致的 queryKey 与 queryFn交给组件内的 Hook确保两者一定命中同一条缓存。关于这些函数在组件内与useUtils配合的更详尽选项说明请继续阅读 useUtils.md若需精确控制过滤条件例如invalidate时按输入值精确匹配可结合 getQueryKey 获取标准 query key 使用。如果发现tanstack/react-query中有你需要的函数尚未被 tRPC 封装不必阻塞你可以直接从tanstack/react-query导入该函数配合 getQueryKey 得到的 queryKey 在过滤器中使用。边界与注意事项把createTRPCQueryUtils用于生产环境前请再次核对以下要点不要在 React 组件内使用它没有useCallback与useQueryClient的加持组件内请用useUtils它是useUtils之外的官方推荐路径。client 与 queryClient 必须来自同一体系传给createTRPCQueryUtils的client通常来自trpc.createClient()vanilla 客户端风格而组件内 Hook 经由QueryClientProvider使用同一QueryClient两者共享缓存语义才成立。Server-side 数据隔离Remix/SSR 中务必每个请求新建queryClient避免跨请求串数据。确认版本环境createTRPCQueryUtils、queryOptions/infiniteQueryOptions等 API 均属于本仓库所代表的 tRPC v11TanStack Query v5集成形态使用旧版 v10 API如useContext的项目需参照 migrate-from-v10-to-v11 进行调整。测试与仓库证据行为即契约仓库中 createQueryUtils.test.ts 为createTRPCQueryUtils提供了系统性的行为验证可当作活的 API 文档研读。几个值得关注的用例ensureData()只发一次网络请求测试先ensureData(1)预取随后再次调用以及setData更新后再次调用断言底层 resolver 仅被调用 1 次expect(factory.resolvers.postById.mock.calls.length).toBe(1)证明命中缓存不再请求的语义见 createQueryUtils.test.ts 首个用例fetch/prefetch/fetchInfinite/prefetchInfinite验证参数透传包括trpc.context等自定义上下文能一路送达链接层invalidate、refetch触发新请求invalidate(1, { refetchType: all })后 resolver 调用数变为 2佐证失效重取行为cancel可中止请求通过自定义慢速queryFn验证调用cancel后 resolver 未被调用setMutationDefaults/getMutationDefaults验证 mutation 默认值可读写且回调能拿到类型正确的canonicalMutationFn。这些用例同时出现在组件的其他能力如useUtils所共享的 shared/proxy/utilsProxy.ts 实现之上说明代理对象 扁平函数 QueryClient的架构是组件内、组件外两条路径共同的地基——理解createTRPCQueryUtils也就理解了useUtils的实现骨架。小结createTRPCQueryUtils是 tRPC 在组件外管理缓存这一问题上给出的标准答案以显式传入的queryClient与client换取脱离 React 渲染上下文的自由。它适合 react-router loader 预取、SSR 数据准备等所有需要先取数、再喂组件的场景与useUtils共享同一套 helpers 语义与类型推导。记住三条主线即可快速上手位置决定选择组件内用useUtils组件外用createTRPCQueryUtils数据靠 queryClient 接力loader 里ensureData()写缓存组件里initialData读缓存多请求场景务必隔离SSR/Remix 为每个请求新建queryClient。掌握它之后你可以继续阅读 useUtils 完整文档 了解各 helper 的选项细节或直接浏览 react-query 测试目录 与 源码在真实用例中验证这些 API 的边界行为。【免费下载链接】trpc‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考