You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js:如何在Server Component中正确使用tRPC

在Next.js App Router的Server Component中使用tRPC的正确姿势

先解答你疑惑的点:网上教程普遍用tRPC Client Hooks,是因为这类钩子上手快,能快速实现客户端交互(比如分页、实时刷新),适合大部分前端场景。但如果需要SSR/SSG优化首屏性能,Server Component中直接调用tRPC服务端逻辑才是更优方案。

下面针对你提到的两种方法的问题逐一解决:

方法1:createTRPCProxyClient的noStore()问题

首先要明确:createTRPCProxyClient本质是模拟客户端网络请求的工具,本来是给Client Component用的。在Server Component中用它属于“绕远路”,这也是为什么需要noStore()的原因——它会触发Next.js的动态渲染逻辑,导致生产构建时冲突。

解决办法:替换为createCaller(放弃方法1)

直接用tRPC提供的createCaller调用服务端逻辑,不需要走网络请求,也完全不需要noStore()。如果你坚持要用createTRPCProxyClient,那必须确保你的调用完全不依赖任何动态上下文(比如headers()、cookies()),并且在页面中声明静态渲染配置:

// app/page.tsx
export const dynamic = 'force-static'; // 强制静态渲染

export default async function Home() {
  // 移除noStore()
  const serverApi = createTRPCProxyClient({ /* 无动态依赖的配置 */ });
  const posts = await serverApi.posts.all.query({});
  
  return <ClientPosts initialData={posts} />;
}

但还是强烈建议换成createCaller,更高效也更符合Server Component的设计。

方法2:createCaller的静态渲染报错

报错Page couldn't be rendered statically because it used 'headers'的核心原因是:你创建caller时依赖了headers()这个动态函数,Next.js会判定该页面无法静态生成。

分场景解决:

场景1:查询不需要用户上下文/请求头

如果你的tRPC路由不需要用户认证、请求头等动态信息,创建caller时传入静态上下文即可:

// server/trpc/server.ts
import { appRouter } from './router';

// 创建静态上下文的caller
export const getStaticCaller = () => {
  return appRouter.createCaller({
    db: /* 你的数据库实例 */,
    session: null, // 不需要用户会话时传null
  });
};

然后在Server Component中直接调用:

// app/page.tsx
import { getStaticCaller } from '@/server/trpc/server';

export default async function Home() {
  const caller = getStaticCaller();
  const initialTweets = await caller.tweet.infiniteFeed({});
  
  // 直接渲染,或传给Client Component做交互
  return (
    <div>
      {initialTweets.map(tweet => <div key={tweet.id}>{tweet.content}</div>)}
      <TweetFeed initialData={initialTweets} />
    </div>
  );
}

这样页面就能正常静态构建了。

场景2:查询必须依赖请求头/用户上下文

如果你的查询需要用户会话、请求头等动态数据,那么这个页面无法静态渲染,需要明确告诉Next.js:

// app/page.tsx
import { getDynamicCaller } from '@/server/trpc/server';

// 声明页面为动态渲染
export const dynamic = 'force-dynamic';
// 或者设置重验证时间(按需选择)
// export const revalidate = 60;

export default async function Home() {
  const caller = await getDynamicCaller();
  const userTweets = await caller.tweet.infiniteFeed({});
  
  return <div>{userTweets.map(tweet => <div key={tweet.id}>{tweet.content}</div>)}</div>;
}

其中getDynamicCaller的实现可以复用你原本的上下文创建逻辑:

// server/trpc/server.ts
import { appRouter } from './router';
import { createTRPCContext } from './trpc';

export const getDynamicCaller = async () => {
  const ctx = await createTRPCContext({
    headers: await headers(),
  });
  return appRouter.createCaller(ctx);
};

最佳实践总结

  1. Server Component中优先用createCaller:直接调用服务端逻辑,无网络开销,性能最优。
  2. 静态/动态页面区分处理:不需要动态上下文的查询用静态caller,允许静态渲染;需要的话声明动态渲染配置。
  3. 客户端交互补充:如果需要客户端的tRPC Hooks(比如分页、刷新),把Server Component获取的initialData传给Client Component的useQuery,并配置staleTime避免重复请求:
// components/TweetFeed.tsx(Client Component)
'use client';

import { api } from '@/trpc/react';

export default function TweetFeed({ initialData }: { initialData: Tweet[] }) {
  const { data: tweets } = api.tweet.infiniteFeed.useQuery({}, {
    initialData,
    staleTime: 60 * 1000, // 1分钟内视为新鲜数据
  });
  
  return <div>{tweets.map(tweet => <div key={tweet.id}>{tweet.content}</div>)}</div>;
}

内容的提问来源于stack exchange,提问作者Влад Рогов

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 04:18:31