tRPC 与 Next.js 静态站点生成(SSG):在 getStaticProps 中预取与脱水 tRPC 查询的完整指南
tRPC(本仓库即 GitHub_Trending/tr/trpc 所示范的 typesafe RPC 方案)与 Next.js Pages Router 结合时,静态站点生成(Static Site Generation,SSG)需要在每个页面的 getStaticProps 内执行 tRPC 查询并预填客户端缓存,才能使生成出的 HTML 直接包含数据、让首屏渲染无需再次请求。本指南将基于 tRPC v10 官方文档体系,讲解如何使用 createServerSideHelpers 预取(prefetch)、脱水(dehydrate)查询并把结果经由 trpcState 交给客户端 useQuery,同时说明 getStaticPaths、revalidate、fallback: 'blocking' 以及 react-query 重取(refetch)策略的完整配置。读完你将能在自己的 Next.js 应用中写出可复用的 SSG + tRPC 页面,并理解其底层机制。
一、SSG 场景下 tRPC 数据流的整体思路
与 SSR 不同,SSG 的 getStaticProps 在构建期运行,产物是纯静态 HTML。tRPC 官方文档(ssg.md)明确指出:
Static site generation requires executing tRPC queries inside
getStaticPropson each page.
也就是说,SSG 需要借助 服务端 helpers(server-side helpers) 完成如下三步:
- 预取:在
getStaticProps中调用 helpers 上的prefetch等方法,在服务端执行查询并写入 react-query 缓存; - 脱水:用
helpers.dehydrate()把查询缓存序列化成一个 plain object; - 回传:将该对象放到页面 props 的
trpcState字段中返回;客户端组件的useQuery会自动识别trpcState并把它作为初始值使用。
这三步在仓库的参考实例中得到了完整印证:examples/next-prisma-todomvc/src/pages/[filter].tsx 的 getStaticProps 中先调用 await ssg.todo.all.prefetch(),再返回 { props: { trpcState: ssg.dehydrate(), ... } }(见 examples/next-prisma-todomvc/src/pages/[filter].tsx)。
二、服务端 helpers:预取能力的入口
createServerSideHelpers 是整个过程的核心入口,它返回一个结构与 tRPC 客户端几乎一致的代理对象(所有 router 名均为键)。与客户端不同,你拿到的不是 useQuery/useMutation,而是以下四个函数(详见 server-side-helpers.md):
| 函数 | 语义 | 典型用途 |
|---|---|---|
prefetch |
执行查询、写入缓存,不返回结果且永不抛错 | 预取客户端必然要用到的数据 |
fetch |
类似普通函数调用,返回查询结果 | 想在服务端直接使用返回值(如拼装页面) |
prefetchInfinite |
预取无限滚动查询(带 cursor 分页) | 无限列表页 SSG/SSR |
fetchInfinite |
服务端执行并返回无限查询结果 | 同上,但需要在服务端取用结果 |
其经验法则为:客户端需要的查询用 prefetch,服务端自身要用返回值的查询用 fetch。这些函数本质上是对 react-query 函数的封装:prefetch 负责把 promise 写入 QueryClient 缓存,供随后的 dehydrate() 提取。当前仓库主干(v11)源码中,这类“代理封装 + 预取入缓存”的模式体现在 packages/react-query/src/rsc.tsx(其内部维护 HELPERS = ['prefetch', 'prefetchInfinite'] 并分别路由到 queryClient.prefetchQuery/prefetchInfiniteQuery),可作为理解其机制结构的参考。
根据是否与 tRPC router 同处一个进程,有两种接入方式:
- 方式一(内部 router,单体内置 API):直接传入
router与ctx。此时 helpers 会在服务端直接调用 procedure,不经过 HTTP 请求(类似 服务端调用 server-side calls 的思路)。也正因如此,你通常拿不到req/res,官方建议使用“inner/outer context”分层(见 server 端 context 文档),传入不含req/res的内部 context。 - 方式二(外部 router,API 独立部署):先创建
createTRPCProxyClient走 HTTP 链路,再把该 client 传给 helpers,适用于前后端分离的部署形态。
三、在 getStaticProps 中预取并在 getStaticPaths 中声明路径
以动态路由 pages/posts/[id].tsx 为例,tRPC v10 文档给出如下完整骨架,其要点是:
- 用
createServerSideHelpers({ router: appRouter, ctx, transformer })创建 helpers; - 通过
context.params?.id拿到路由参数并await helpers.post.byId.prefetch({ id }); - 返回
props时必须使用trpcState作为键,并附上revalidate: 1(每 1 秒增量再生成,即 ISR 能力); - 用
getStaticPaths先查库枚举全部帖子 id;fallback: 'blocking'表示未预先声明的路径在首访时由服务端渲染并缓存,浏览器端不会经历 loading。
import { createServerSideHelpers } from '@trpc/react-query/server';
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 helpers = createServerSideHelpers({
router: appRouter,
ctx: {},
transformer: superjson, // optional - adds superjson serialization
});
const id = context.params?.id as string;
// prefetch `post.byId`
await helpers.post.byId.prefetch({ id });
return {
props: {
trpcState: helpers.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/pages/api-reference/functions/get-static-paths#fallback-blocking
fallback: 'blocking',
};
};
export default function PostViewPage(
props: InferGetStaticPropsType<typeof getStaticProps>,
) {
const { id } = props;
const postQuery = trpc.post.byId.useQuery({ 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>
</>
);
}
关于 revalidate:上述示例返回了 revalidate: 1,即构建出静态页后,Next.js 仍会以 1 秒为窗口在后台增量重生成(ISR),兼顾静态性能与数据新鲜度。若希望页面在构建后永久静态、绝不重生成,则不返回该字段即可。
关于 transformer:代码中 transformer: superjson 为可选配置,但它对 SSG 非常重要——只有与客户端一致的 transformer,才能正确序列化 Date、Map 等非 JSON 类型(示例中 data.createdAt.toLocaleDateString(...) 依赖服务端以 Date 类型预取并回传)。transformer 的底层机制见 data-transformers 文档。
四、组件消费:useQuery 自动拾取 trpcState
页面组件中的 trpc.post.byId.useQuery({ id }) 不需要任何特殊处理:tRPC 的 createTRPCNext 客户端在挂载时自动检查 props 中是否存在 trpcState,若存在则将其作为 react-query 缓存的初始状态,因此查询会立即处于 success,不会闪现 loading。正因如此,示例里 postQuery.status !== 'success' 的分支在 fallback: "blocking" 下“不会发生”——这段代码只是防御性写法。
这一“预取即缓存、脱水即初始态”的设计,使静态生成的数据能无缝被客户端查询接管。仓库参考实例中同样可以看到该机制被刻意复用:examples/next-prisma-todomvc/src/server/ssg-init.ts 把 createServerSideHelpers<AppRouter>({ router: appRouter, ctx: await createInnerTRPCContext({}), transformer: SuperJSON }) 封装成 ssgInit(context),页面只需 const ssg = await ssgInit(context); await ssg.todo.all.prefetch();,随后页面上 trpc.todo.all.useQuery(undefined, { staleTime: 3000 }) 直接读取预取结果,注释明确写着"页面会以服务端数据渲染,客户端不会有 loading 状态"(见 examples/next-prisma-todomvc/src/pages/[filter].tsx)。
把 SSG 初始化抽成独立模块(而非在每个页面重复拼装 helpers)是 tRPC 官方参考工程推荐的实践:你可以在 examples/next-prisma-todomvc/src/server/ssg-init.ts 看到完整封装,包括注释中给出的"外部 router"(走 httpBatchLink 的独立 API)替换方案。
五、控制客户端重取:refetchOnMount 与 refetchOnWindowFocus
文档特别提醒了一个默认行为陷阱:react-query(tRPC 客户端的底层库)的默认行为是组件挂载时在客户端重新拉取数据。因此如果希望数据只在构建期通过 getStaticProps 获取、浏览器端不再发起 API 请求,就必须关闭 refetchOnMount 与 refetchOnWindowFocus。这在调用第三方限流 API(如按次计费/限速的 SaaS 数据源)时尤为重要——它能将每次页面访问的请求数压到最低甚至为零。
方式一:按单个查询关闭
const data = trpc.example.useQuery(
// if your query takes no input, make sure that you don't
// accidentally pass the query options as the first argument
undefined,
{ refetchOnMount: false, refetchOnWindowFocus: false },
);
注意上方注释强调的细节:无输入参数的查询,第一个参数必须显式传 undefined,否则查询选项会被误当成输入传给 procedure,类型与运行时都会出错。
方式二:全局统一关闭
如果应用中所有查询都希望遵循同一策略(纯静态站点常见),可以在创建 tRPC Next 客户端的 queryClientConfig.defaultOptions.queries 里统一配置:
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import superjson from 'superjson';
import type { AppRouter } from './api/trpc/[trpc]';
export const trpc = createTRPCNext<AppRouter>({
config(config) {
return {
transformer: superjson,
links: [
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
}),
],
// Change options globally
queryClientConfig: {
defaultOptions: {
queries: {
refetchOnMount: false,
refetchOnWindowFocus: false,
},
},
},
};
},
});
queryClientConfig 会被 tRPC 原样透传给底层的 react-query QueryClient,因此这里可以填写任何 QueryClient 支持的 defaultOptions(如 staleTime 等)。注意:全局关闭后,如果某些页面需要实时性,应在该页查询上单独传 refetchOnMount: true 覆盖。
使用全局策略的取舍
文档在结尾特别提示:当应用同时存在静态页面与动态数据页面时,务必谨慎使用全局关闭。例如登录后的用户面板需要每次挂载都拉取最新数据,此时全局 refetchOnMount: false 会导致其数据过期。推荐做法是按需组合:
- 整站以静态为主 → 全局关闭,个别动态查询单独开启;
- 静态/动态混合 → 保持默认重取行为,仅在静态页面内部针对具体查询显式关闭。
六、把模式落地的仓库参考
官方 v10 文档(ssg.md)在开头引用了专为此场景打造的参考工程:本仓库中的 examples/next-prisma-todomvc/(TodoMVC + Prisma + Next.js Pages Router)即对应实现,其配套 README.md 位于 examples/next-prisma-todomvc/README.md。建议按以下顺序阅读:
- 预取封装:examples/next-prisma-todomvc/src/server/ssg-init.ts — 理解
createServerSideHelpers与 inner context、superjson 的组装方式; - 页面接入:examples/next-prisma-todomvc/src/pages/[filter].tsx — 观察
getStaticPaths、getStaticProps、trpcState、revalidate的完整写法; - tRPC 客户端:该工程的
utils/trpc.ts— 确认客户端侧createTRPCNext的 transformer 与 links 配置与服务端 helpers 一致(transformer 不一致会导致水合失败)。
若你的查询是无限分页类型(如动态路由的评论列表),只需把文档示例中的 prefetch 换成 prefetchInfinite,并在页面中使用 useInfiniteQuery,其余流程(脱水、回传 trpcState、客户端自动拾取)完全一致。
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