如何在NextJS生产环境中正常使用Relay并实现SSR适配?
Next.js 接入 Relay 问题解决方案
构建阶段意外发起GraphQL请求修复
- 强制指定页面渲染模式,阻断静态预生成逻辑
- 若使用App Router,在对应动态页面顶部添加配置:
export const dynamic = 'force-dynamic'
该配置会告知Next.js不要在build阶段预渲染该页面,所有逻辑均在请求到达时执行。 - 若使用Pages Router,确保页面仅导出
getServerSideProps、不要导出getStaticProps/getStaticPaths,只要页面存在getServerSideProps导出,Next.js会自动跳过该页面的构建阶段静态生成。
- 若使用App Router,在对应动态页面顶部添加配置:
- 排查全局初始化逻辑:不要在
_app.tsx/_document.tsx/根布局组件的顶层同步执行Relay查询,这类逻辑会在构建阶段执行,触发对GraphQL服务的请求。
Relay SSR 适配落地实践
Hook无法在getServerSideProps中使用、环境实例无法复用的问题,本质是Relay SSR的常见认知误区:Relay的SSR逻辑不需要跨服务端数据预取逻辑和React组件上下文共享同一个环境实例——服务端每个请求本身就是独立上下文,全局单例的Relay环境反而会造成多用户间缓存数据串流的严重问题。
正确实现思路是:每次服务端请求创建独立的Relay环境完成数据预取,将预取后的缓存数据序列化传到前端,前端初始化环境时注入这部分缓存完成注水,前后端状态完全对齐。
具体实现步骤
- 封装Relay环境工厂函数,禁止导出全局单例
// src/relay/createRelayEnvironment.js import { Environment, Network, RecordSource, Store } from 'relay-runtime'; /** * 创建Relay环境实例 * @param {Object} requestHeaders - 请求头,用于服务端透传cookie、鉴权信息 */ export function createRelayEnvironment(requestHeaders = {}) { const network = Network.create(async (operation, variables) => { const response = await fetch('https://localhost:3000/api/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', ...requestHeaders, }, body: JSON.stringify({ query: operation.text, variables, }), }); return response.json(); }); const recordSource = new RecordSource(); const store = new Store(recordSource); return new Environment({ network, store }); }
- Pages Router 下
getServerSideProps预取逻辑实现
// pages/[your-dynamic-route].js import { loadQuery } from 'react-relay'; import { createRelayEnvironment } from '@/relay/createRelayEnvironment'; import PageQuery from '@/__generated__/PageQuery.graphql'; export async function getServerSideProps(context) { // 基于当前请求头创建独立的Relay环境 const relayEnv = createRelayEnvironment(context.req.headers); // 预取页面依赖的查询,变量从路由参数、请求上下文里取即可 await loadQuery(relayEnv, PageQuery, { pageId: context.params.pageId, }); // 提取环境中已缓存的查询结果,序列化后作为props传给前端 const initialRecords = relayEnv.getStore().getSource().toJSON(); return { props: { initialRecords, queryVariables: { pageId: context.params.pageId, }, }, }; }
- 前端环境注水与Provider注入
修改自定义_app组件,用服务端传回的缓存数据初始化前端Relay环境,避免客户端重复发起请求:
// pages/_app.js import { RelayEnvironmentProvider } from 'react-relay'; import { createRelayEnvironment } from '@/relay/createRelayEnvironment'; import { RecordSource } from 'relay-runtime'; import { useMemo } from 'react'; export default function App({ Component, pageProps }) { const relayEnvironment = useMemo(() => { const env = createRelayEnvironment(); // 注入服务端预取的缓存数据 if (pageProps.initialRecords) { env.getStore().publish(new RecordSource(pageProps.initialRecords)); } return env; }, [pageProps.initialRecords]); return ( <RelayEnvironmentProvider environment={relayEnvironment}> <Component {...pageProps} /> </RelayEnvironmentProvider> ); }
- 页面组件消费数据
直接在页面组件中调用usePreloadedQuery即可,此时数据已经存在于Relay缓存中,不会发起重复请求:
// pages/[your-dynamic-route].js 续 import { usePreloadedQuery } from 'react-relay'; export default function DynamicPage({ queryVariables }) { const pageData = usePreloadedQuery(PageQuery, queryVariables); // 正常使用pageData渲染即可 return <div>{/* 页面业务逻辑 */}</div>; }
踩坑提醒
- 客户端路由跳转预加载场景,直接在组件内调用
useRelayEnvironment()拿到Provider注入的环境实例,再调用loadQuery做预取即可,这部分是纯客户端逻辑,不影响SSR流程。 - 若使用App Router,可直接在Server Component中创建Relay环境完成数据查询,将序列化后的缓存和数据传给Client Component内的RelayProvider即可,不需要依赖
getServerSideProps。 - 服务端环境必须按请求创建,不能复用单例,否则会出现用户间数据泄露的问题。
内容的提问来源于stack exchange,提问作者Jessica Wentling
相关产品推荐
相关产品推荐

