Next.js:如何在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); };
最佳实践总结
- Server Component中优先用
createCaller:直接调用服务端逻辑,无网络开销,性能最优。 - 静态/动态页面区分处理:不需要动态上下文的查询用静态caller,允许静态渲染;需要的话声明动态渲染配置。
- 客户端交互补充:如果需要客户端的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,提问作者Влад Рогов

