tRPC React Suspense 实战指南:useSuspenseQuery 与 useSuspenseInfiniteQuery 详解
tRPC React Suspense 实战指南useSuspenseQuery 与 useSuspenseInfiniteQuery 详解【免费下载链接】trpc♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpctrpc/react-query为 React 组件提供了基于Suspense的取数 hooks让查询在数据就绪前自动暂停渲染彻底告别手工维护isLoading分支。本文基于 tRPC v10 版本文档深入讲解useSuspenseQuery()、useSuspenseInfiniteQuery()的使用方式、返回结构、使用前提与常见陷阱并结合仓库中trpc/react-query的源码与测试用例说明其底层原理。读完你将掌握在 React 组件中以声明式、无 loading 闪烁的方式消费类型安全的 tRPC 查询以及如何在无限滚动场景中使用 Suspense 版的分页查询。使用前提与注意事项在把 Suspense hooks 引入项目之前请先确认如下两条来自官方文档的前提条件确保 React 处于最新版本Suspense 查询依赖 React 18 的并发渲染特性请将react/react-dom升级到最新稳定版谨慎搭配 Next.js 自动 SSR如果在 Next.js 中把 Suspense 与 tRPC 的automatic SSR即ssr: true组合使用一旦某个查询在服务端失败整页会在服务端崩溃即使外层包了ErrorBoundary /也无法挽救ssr.md。第二条的限制根源在于自动 SSR 期间数据在服务端就已挂起服务端渲染过程中抛出的错误发生在 HTML 流式输出之前ErrorBoundary此时无从介入因此需要格外注意查询失败场景的处理。返回值是一个 Tuple[data, query]useSuspenseQuery与useSuspenseInfiniteQuery的返回值与普通useQuery不同它们返回一个[data, query]元组tuple而非单一的查询结果对象。这一设计的目的文档中的 tip 明确指出是把数据直接放在解构的第一个位置方便你立即使用数据同时可以给第二个元素随意重命名为有语义的变量名例如const [post, postQuery] trpc.post.byId.useSuspenseQuery({ id: 1 }); // post —— 已就绪的数据本身 // postQuery —— 完整的查询结果对象isFetching、refetch 等这也意味着只要组件能够执行到 Suspense hooks 之后的代码post就必然有值不再需要if (isLoading) return Spinner /之类的防御性判断。准备一个后端 Router 示例为了演示两条 Suspense hooks文档给出了一个同时包含单条查询与游标分页查询的服务端示例。其中post.byId使用 zod 校验id输入查不到时抛出NOT_FOUND的TRPCErrorpost.all接收可选的cursor并返回下一页游标nextCursorimport { initTRPC, TRPCError } from trpc/server; import { z } from zod; const t initTRPC.create(); const posts [ { id: 1, title: everlong }, { id: 2, title: After Dark }, ]; const appRouter t.router({ post: t.router({ all: t.procedure .input( z.object({ cursor: z.string().optional(), }) ) .query(({ input }) { return { posts, nextCursor: 123 as string | undefined, }; }), byId: t.procedure .input( z.object({ id: z.string(), }) ) .query(({ input }) { const post posts.find(p p.id input.id); if (!post) { throw new TRPCError({ code: NOT_FOUND, }) } return post; }), }), }); export type AppRouter typeof appRouter;在客户端通过createTRPCReactAppRouter()创建带完整类型推断的trpc对象setup.mdx 中有完整初始化说明import { createTRPCReact } from trpc/react-query; import type { AppRouter } from ../server; export const trpc createTRPCReactAppRouter();之后凡是useSuspenseQuery等 hooks其输入参数都会被后端input的 zod schema 严格约束输入拼错或类型不匹配会在编译期直接报错。useSuspenseQuery()单条数据的声明式取数useSuspenseQuery(input, opts?)是useQuery的 Suspense 版本。调用后组件会挂起直到查询解析完成才真正渲染若查询抛出错误则由最近的ErrorBoundary捕获并展示错误 UI。import React from react; import { trpc } from ../utils/trpc; function PostView() { const [post, postQuery] trpc.post.byId.useSuspenseQuery({ id: 1 }); // 此处 post 已经就绪直接渲染数据 return {post.title}/; }值得注意的是元组第二位的postQuery它携带完整的查询信息例如refetch、isFetching、isRefetching等便于在需要刷新或后台重新拉取的场景继续使用而不影响首屏的 Suspense 体验。底层实现如何挂起在仓库的trpc/react-query实现中这一层其实是直接委托给 TanStack Query 的同名 hooks。以 packages/react-query/src/shared/hooks/createHooksInternal.tsx 中生成的useSuspenseQuery为例它内部计算出 tRPC 专属的queryKey将请求封装为queryFn后调用 TanStack 的__useSuspenseQuery并把 abort 信号、trpc.context等选项透传到底层 clientconst hook __useSuspenseQuery( { ...opts, queryKey: queryKey as any, queryFn: (queryFunctionContext) { const actualOpts { ...opts, trpc: { ...opts?.trpc, ...(shouldAbortOnUnmount ? { signal: queryFunctionContext.signal } : { signal: null }), }, }; return context.client.query(...getClientArgs(queryKey, actualOpts)); }, }, context.queryClient, ); hook.trpc useHookResult({ path }); return [hook.data, hook as any]; // ← Tuple 返回结构可以看到挂起由 TanStack Query 的 Suspense 机制负责tRPC 主要负责三件事把过程路径 输入换算成稳定的queryKey、把 tRPC client 调用包装为queryFn、最后把结果以[data, query]元组形式返回。在 createTRPCReact.tsx 的类型定义中useSuspenseQuery的类型签名亦与文档一致。useSuspenseInfiniteQuery()Suspense 版的无限滚动分页useSuspenseInfiniteQuery是useInfiniteQuery的 Suspense 版本用于「下拉加载更多」这类游标分页场景。前提是你的后端 procedure 必须接收一个cursor输入类型不限string / number 均可客户端才能以此驱动翻页。import React from react; import { trpc } from ../utils/trpc; function PostView() { const [pages, allPostsQuery] trpc.post.all.useSuspenseInfiniteQuery( {}, { getNextPageParam(lastPage) { return lastPage.nextCursor; }, }, ); const { isFetching, isFetchingNextPage, fetchNextPage, hasNextPage } allPostsQuery; return ( {/* pages.posts 渲染列表 */} button onClick{() fetchNextPage()} disabled{!hasNextPage || isFetchingNextPage} Load more /button / ); }关键参数说明第一个入参是固定输入{}对应后端post.all的z.object({ cursor: ... })中除cursor之外的部分由于这里cursor由框架内部管理输入对象里无需也不应传cursorgetNextPageParam(lastPage)从上一页返回值中取出下一页游标。当后端返回nextCursor为undefined示例中的123 as string | undefined时TanStack Query 会判定没有更多页面从而令hasNextPage为false解构出的pages就是所有已加载页面的聚合结果其形状为{ pages, pageParams }可通过pages.flatMap((p) p.posts)之类的方式拍平后渲染第二位allPostsQuery暴露isFetching、isFetchingNextPage、fetchNextPage、hasNextPage等用于实现「加载更多」按钮或无限滚动触发器。关于useInfiniteQuery更完整的分页 procedure 写法如 Prisma 游标分页、limit校验、nextCursor计算可参考 useInfiniteQuery.md。常见陷阱与经验陷阱一查询失败会抛给 ErrorBoundary而非返回 error 状态这是 Suspense 模式与普通查询最大的行为差异数据加载中的未就绪不再以 status 表达而是直接抛给 React 的 Suspense 机制失败则抛给ErrorBoundary。因此渲染层代码可以完全忽略 loading/error 状态只写数据已就绪的视图错误 UI 需要由上层的ErrorBoundary统一提供。陷阱二不要再把返回元组当 query 对象整体解构const { data } useSuspenseQuery(...)这种写法与返回结构不匹配。请使用元组解构const [data, query] ...。仓库的升级工具中甚至专门为这一写法提供了自动化转换测试用例见 packages/upgrade/test/fixtures/hooks/suspenseDestructuring.tsx 及其.snap.tsx侧面印证了旧式对象解构代码在向新返回结构迁移时是常见问题。陷阱三Next.js 自动 SSR 下的服务端崩溃回到开篇的警告当项目开启了 tRPC 的自动 SSR全局ssr: trueSuspense 查询在服务端请求阶段抛错时整页渲染会在服务端崩溃ErrorBoundary /对此无能为力。如果你依赖服务端兜底展示错误请评估该查询的失败率或考虑为非关键查询单独关闭 SSR。与测试用例互相印证仓库测试 useSuspenseQuery.test.tsx 是理解该 hook 行为最直接的源码证据。它以最小post.byIdprocedure 验证了三点返回结构与类型const [data, query1] ...useSuspenseQuery({ id: 1 })中data与query1.data都被正确推断为服务端返回的__result且渲染后 DOM 中出现该文本trpc.context传递在第二个参数中传入{ trpc: { context: { test: true } } }底层 link 收到的请求上下文确实包含该字段断言ctx.spyLink.mock.calls[0]?.[0].context不支持skipTokenuseSuspenseQuery(skipToken)在类型层面被ts-expect-error标注拒绝——因为条件性挂起与 Suspense 的语义相矛盾。可见文档描述的 hooks 行为均有源码与测试背书读者可放心按上述模式接入自己的应用。小结在 React 18 中为 tRPC 查询启用 Suspense只需把useQuery换成useSuspenseQuery、把useInfiniteQuery换成useSuspenseInfiniteQuery再把结果按[data, query]元组解构即可。它消除了手写 loading 分支的样板代码让数据就绪才渲染这一心智模型由 React 并发特性替你完成。使用中请牢牢记住两条红线保持 React 为最新版本以及谨慎在 Next.js 自动 SSR 下组合使用。【免费下载链接】trpc♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考