tRPC v9 服务端数据预取指南:全面掌握 createSSGHelpers 与 Next.js SSG/SSR 集成
本文聚焦 tRPC(以仓库中 version-9.x SSG Helpers 文档 为主体)在 v9 时代提供的关键服务端工具 createSSGHelpers,讲解如何借助它在 Next.js 的 getServerSideProps / getStaticProps 阶段提前执行 tRPC 查询、序列化 React Query 缓存并完成客户端水合(hydration)。读完你将能够:理解 prefetchQuery、fetchQuery、dehydrate 等辅助函数各自的职责与差异,写出可运行的 SSR/SSG 页面,并理清该 API 在 tRPC v9 → v10 → v11 演进中的对应关系。
createSSGHelpers:在服务端为 React Query 预取数据的入口
createSSGHelpers(全称 Server-Side Generation Helpers,即"服务端生成辅助函数")是 tRPC v9 提供给 Next.js 等 SSR/SSG 场景的桥接工具。它的核心作用正如官方文档开篇所述:
createSSGHelpersprovides you a set of helper functions that you can use to prefetch queries on the server.
在浏览器端,页面通过 useQuery 发起 tRPC 请求;而服务端渲染或静态生成阶段没有浏览器、没有 React Query 的运行时挂载,因此需要一种"在服务端先把数据请求执行完,再把结果打包交给客户端复用"的手段。createSSGHelpers 正是负责这件事:它接收你的 router、context 以及可选的数据 transformer,返回一组经过类型约束的辅助函数,这些函数内部全部是对 react-query 方法的封装。
基础调用形态
官方文档给出的最小初始化代码如下(出处:ssg-helpers.md):
import { createSSGHelpers } from '@trpc/react/ssg';
const {
prefetchQuery,
prefetchInfiniteQuery,
fetchQuery,
fetchInfiniteQuery,
dehydrate,
queryClient,
} = await createSSGHelpers({
router: appRouter,
ctx: createContext,
transformer: superjson, // optional - adds superjson serialization
});
几点需要注意:
await语义:createSSGHelpers在 v9 中是异步工厂,ctx既可传createContext函数本身,也可传await createContext()之后的 context 对象(见下文 Next.js 示例,两种写法文档中都有出现)。router:传入应用的根路由(如appRouter),它决定了后续字符串形式的 procedure 路径(如'post.byId')能被强类型校验。ctx:服务端 context。若页面需要鉴权、数据库访问等上下文,应在此传入与运行时一致的创建方式。transformer(可选):如superjson,用于序列化Date、Map、Set等 JSON 无法直接表达的类型。必须与服务端 HTTP 处理器、客户端links中配置的 transformer 保持一致,否则反序列化会错位。
返回的六个成员各自用途
| 返回值 | 类型/职责 | 关键差异 |
|---|---|---|
prefetchQuery |
预取普通查询 | 不返回查询结果,只是把数据写入服务端 QueryClient 缓存,供 dehydrate() 打包 |
fetchQuery |
取回普通查询 | 返回查询结果本身,适合既要在服务端拿到值做逻辑(如 404 判断)又想缓存到客户端的场景 |
prefetchInfiniteQuery |
预取无限滚动查询(配合 useInfiniteQuery) |
需要按页面参数形式传入,缓存的是分页结构 |
fetchInfiniteQuery |
取回无限滚动查询 | 返回多页数据,供服务端消费 |
dehydrate |
序列化整个 React Query 缓存 | 输出可塞入 props.trpcState 的普通对象,随页面 HTML 一起下发 |
queryClient |
暴露底层 react-query 的 QueryClient | 需要执行更底层操作(如 setQueryData)时可直接使用 |
官方文档特别强调:所有返回函数本质上都是 react-query 对应方法的薄封装,因此其缓存键语义、失效规则、重试机制均继承自 react-query。这一点在仓库当前的测试中依然有直接体现,例如 ssg.test.ts 中围绕 createSSGHelpers(v11 时代的同名 API)验证了查询键与脱水缓存的一致性。
在 getServerSideProps 中使用:完整 SSR 示例拆解
文档给出了一个非常典型的"文章详情页"示例,其流程代表了 SSR 场景的完整数据管线。我们逐段剖析:
import { createSSGHelpers } from '@trpc/react/ssg';
import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
import { createContext, prisma } from 'server/context';
import { appRouter } from 'server/routers/_app';
import superjson from 'superjson';
import { trpc } from 'utils/trpc';
export async function getServerSideProps(
context: GetServerSidePropsContext<{ id: string }>,
) {
const ssg = createSSGHelpers({
router: appRouter,
ctx: await createContext(),
transformer: superjson,
});
const id = context.params?.id as string;
/*
* Prefetching the `post.byId` query here.
* `prefetchQuery` does not return the result - if you need that, use `fetchQuery` instead.
*/
await ssg.prefetchQuery('post.byId', {
id,
});
// Make sure to return { props: { trpcState: ssg.dehydrate() } }
return {
props: {
trpcState: ssg.dehydrate(),
id,
},
};
}
export default function PostViewPage(
props: InferGetServerSidePropsType<typeof getServerSideProps>,
) {
const { id } = props;
// This query will be immediately available as it's prefetched.
const postQuery = trpc.useQuery(['post.byId', { id }]);
const { data } = postQuery;
return (
<>
<h1>{data.title}</h1>
<em>Created {data.createdAt.toLocaleDateString()}</em>
<p>{data.text}</p>
<h2>Raw data:</h2>
<pre>{JSON.stringify(data, null, 4)}</pre>
</>
);
}
三步走的数据流
- 初始化 helpers:在
getServerSideProps中创建ssg。这里ctx: await createContext()与基础示例中直接传ctx: createContext的区别在于:你可以先拿到 context(例如完成鉴权后的用户信息)再继续后续逻辑。 - 预取数据:
await ssg.prefetchQuery('post.byId', { id })。v9 使用字符串路径 + 输入参数的调用风格(注意 v9 中还可用数组写法,如第 72 行客户端侧的trpc.useQuery(['post.byId', { id }]))。路径与入参都被router: appRouter约束,写错会直接得到 TypeScript 报错。此调用在服务端真正执行了数据库/HTTP 调用,把结果写入缓存,但不向调用方返回结果。 - 脱水并下发:
props中必须包含trpcState: ssg.dehydrate()。dehydrate()把 React Query 缓存序列化为普通对象,随 Next.js 的props进入页面 HTML;客户端挂载时,withTRPC/createTRPCNext包装层会自动读取该trpcState作为对应useQuery的初始数据。这也是文档注释 "Make sure to return{ props: { trpcState: ssg.dehydrate() } }" 想强调的约定——漏掉trpcState,预取就白做了。
客户端为何"立即可用"
第 71-72 行的注释说明了水合后的体验:"This query will be immediately available as it's prefetched"。因为服务端已经把 post.byId 的数据连同查询键一起脱水,浏览器端同名 useQuery(['post.byId', { id }]) 会命中这份初始缓存,页面首屏渲染即展示数据,无需等待新的网络往返;组件里 data.title、data.createdAt 之所以能直接使用,是因为 superjson 保真了 createdAt 的 Date 类型。
prefetch 与 fetch 的选择:何时用 fetchQuery
文档在示例注释中点明了两者的分界:
prefetchQuerydoes not return the result - if you need that, usefetchQueryinstead.
换句话说:
- 只想"暖缓存"给客户端 → 用
prefetchQuery,服务端不关心返回值; - 服务端需要基于查询结果做分支逻辑(例如数据不存在时
return { notFound: true }、或把结果加工后额外写入 props)→ 用fetchQuery拿到真实数据; - 分页/无限加载数据 → 对应使用
prefetchInfiniteQuery/fetchInfiniteQuery。
dehydrate 的作用对象是整个 QueryClient 缓存,因此即使你在服务端同时预取了多条查询(普通 + 无限),一次 dehydrate() 也能把它们全部打包下发,客户端各条 useQuery / useInfiniteQuery 会自动按查询键取回各自初始数据。
静态生成(SSG)场景:createSSGHelpers + getStaticProps
createSSGHelpers 不只服务 SSR,也是 v9 实现静态站点生成的关键(见同目录下的配套文档 ssg.md)。思路与 SSR 完全相同,只是把调用点换到 getStaticProps,并配合 getStaticPaths 枚举动态路由:
import { createSSGHelpers } from '@trpc/react/ssg';
import {
GetStaticPaths,
GetStaticPropsContext,
InferGetStaticPropsType,
} from 'next';
import { prisma } from 'server/context';
import { appRouter } from 'server/routers/_app';
import superjson from 'superjson';
import { trpc } from 'utils/trpc';
export async function getStaticProps(
context: GetStaticPropsContext<{ id: string }>,
) {
const ssg = await createSSGHelpers({
router: appRouter,
ctx: {},
transformer: superjson, // optional - adds superjson serialization
});
const id = context.params?.id as string;
// prefetch `post.byId`
await ssg.fetchQuery('post.byId', {
id,
});
return {
props: {
trpcState: ssg.dehydrate(),
id,
},
revalidate: 1,
};
}
export const getStaticPaths: GetStaticPaths = async () => {
const posts = await prisma.post.findMany({
select: {
id: true,
},
});
return {
paths: posts.map((post) => ({
params: {
id: post.id,
},
})),
// https://nextjs.org/docs/basic-features/data-fetching#fallback-blocking
fallback: 'blocking',
};
};
export default function PostViewPage(
props: InferGetStaticPropsType<typeof getStaticProps>,
) {
const { id } = props;
const postQuery = trpc.useQuery(['post.byId', { id }]);
if (postQuery.status !== 'success') {
// won't happen since we're using `fallback: "blocking"`
return <>Loading...</>;
}
const { data } = postQuery;
return (
<>
<h1>{data.title}</h1>
<em>Created {data.createdAt.toLocaleDateString('en-us')}</em>
<p>{data.text}</p>
<h2>Raw data:</h2>
<pre>{JSON.stringify(data, null, 4)}</pre>
</>
);
}
此例可以读出几个 SSG 专属要点:
ctx传{}即可:静态生成发生在构建期,无真实请求,因此没有 HTTP 层面的 context;除非路由确实需要(如基于环境变量的数据源),直接传空对象即可。revalidate: 1开启 ISR:让页面在增量静态再生成模式下周期性刷新,适合内容会变化又不想退回 SSR 的场景。fallback: 'blocking'与状态兜底:因为构建期未枚举到的路径会走服务端阻塞式生成,客户端理论上必然已具备数据,因此组件中status !== 'success'的分支只是防御性兜底(这正是 ssg.md 中注释 "won't happen since we're usingfallback: 'blocking'" 的含义)。- 配套的真实实现可参考仓库示例应用 examples/next-prisma-todomvc,它使用 Prisma + superjson,完整演示了这类页面在
src/pages与服务端路由之间的协作方式。
需要留意的 react-query 行为:默认会再次请求
值得强调的是:SSG/SSR 预取只是首屏初始值。react-query 的默认策略是在客户端挂载后重新验证(refetch)数据。若你的页面希望"完全以服务端数据为准、避免重复请求",需要关闭相关重取开关——在 v9→v10 迁移后的文档 client/nextjs/ssg.md 中对此有明确说明:
- 按查询关闭:
trpc.example.useQuery(undefined, { refetchOnMount: false, refetchOnWindowFocus: false })(注意:无输入参数的查询,别把选项对象误当作第一个参数传入); - 全局关闭:在
createTRPCNext的config中通过queryClientConfig.defaultOptions.queries统一设置。
该提示尤其适用于对接第三方限流 API 的场景——每增加一次客户端重复请求就多消耗一次配额。若页面同时混合静态与动态查询,则建议按查询而非全局配置处理,以免影响动态数据的实时性。
API 的版本演进:从 v9 到今天
理解 createSSGHelpers 时容易与仓库当前主版本(v11)混淆,因此有必要厘清它在版本线中的演变。仓库迁移文档 migrate-from-v10-to-v11.mdx 的 "SSG Helpers" 一节给出了权威说明:
- v9:导出
createSSGHelpers(即本文主体,来源为@trpc/react/ssg),采用字符串路径 + 数组参数的调用风格(如ssg.prefetchQuery('post.byId', { id })); - v10:v9 的
createSSGHelpers被移除,改用基于 Proxy 的createProxySSGHelpers,调用风格升级为helpers.post.byId.prefetch({ id })的对象链式写法; - v11:
createProxySSGHelpers更名为createSSGHelpers(旧 Proxy 名保留为向后兼容的别名),且 v10/v11 的导出位置为@trpc/react-query/server,函数名为createServerSideHelpers(见 v10 版 ssg 文档 的示例)。
所以,若你正在阅读 v9 时代的文档或存量代码,请把 @trpc/react/ssg 与字符串路径调用理解为 v9 专属形态;升级时只需按上述映射关系平移 API 名称与调用风格,底层"服务端预取 → dehydrate → trpcState 下发 → 客户端水合"的数据流思想完全一致。
从当前仓库源码结构看,这一能力在现代实现中位于 packages/react-query/src/server/ssgProxy.ts(以 Proxy 形式为每个 procedure 生成 .prefetch / .fetch 等方法),并有 ssg.test.ts、prefetchQuery.test.tsx、dehydrate.test.tsx 等测试佐证其脱水与预取语义,感兴趣的读者可以顺藤摸瓜做源码级阅读。
小结
createSSGHelpers 是 tRPC v9 在 Next.js SSR/SSG 场景下的核心工具:它让你在服务端用类型安全的方式预取任意 procedure,把 React Query 缓存通过 dehydrate() 序列化进 props.trpcState,最终由客户端无缝接管。掌握它的关键在于三点:
- 调用约定:
router+ctx+ 可选transformer初始化;prefetchQuery不返回值、fetchQuery返回值,按服务端是否需要消费结果来选用; - 水合协议:务必在
props中携带trpcState: ssg.dehydrate(),客户端同名useQuery才能拿到首屏初始数据; - 版本意识:v9 的字符串路径 API 在 v10/v11 已演进为 Proxy 式链式 API,迁移对照表见 migrate-from-v10-to-v11.mdx,配合 ssg.md 可进一步了解
getStaticProps侧的完整用法。
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