首页
/ tRPC 与 Next.js 静态站点生成(SSG):在 getStaticProps 中预取与脱水 tRPC 查询的完整指南

tRPC 与 Next.js 静态站点生成(SSG):在 getStaticProps 中预取与脱水 tRPC 查询的完整指南

2026-09-08 16:08:41作者:江焘钦

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,同时说明 getStaticPathsrevalidatefallback: '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 getStaticProps on each page.

也就是说,SSG 需要借助 服务端 helpers(server-side helpers) 完成如下三步:

  1. 预取:在 getStaticProps 中调用 helpers 上的 prefetch 等方法,在服务端执行查询并写入 react-query 缓存;
  2. 脱水:用 helpers.dehydrate() 把查询缓存序列化成一个 plain object;
  3. 回传:将该对象放到页面 props 的 trpcState 字段中返回;客户端组件的 useQuery 会自动识别 trpcState 并把它作为初始值使用。

这三步在仓库的参考实例中得到了完整印证:examples/next-prisma-todomvc/src/pages/[filter].tsxgetStaticProps 中先调用 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):直接传入 routerctx。此时 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,才能正确序列化 DateMap 等非 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.tscreateServerSideHelpers<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 请求,就必须关闭 refetchOnMountrefetchOnWindowFocus。这在调用第三方限流 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。建议按以下顺序阅读:

  1. 预取封装examples/next-prisma-todomvc/src/server/ssg-init.ts — 理解 createServerSideHelpers 与 inner context、superjson 的组装方式;
  2. 页面接入examples/next-prisma-todomvc/src/pages/[filter].tsx — 观察 getStaticPathsgetStaticPropstrpcStaterevalidate 的完整写法;
  3. tRPC 客户端:该工程的 utils/trpc.ts — 确认客户端侧 createTRPCNext 的 transformer 与 links 配置与服务端 helpers 一致(transformer 不一致会导致水合失败)。

若你的查询是无限分页类型(如动态路由的评论列表),只需把文档示例中的 prefetch 换成 prefetchInfinite,并在页面中使用 useInfiniteQuery,其余流程(脱水、回传 trpcState、客户端自动拾取)完全一致。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
899
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525