使用renderToPipeableStream时,如何正确重水合服务端Apollo缓存?
React 18 流式SSR结合Apollo Client的缓存重水合标准方案
问题背景
我正在用React 18的renderToPipeableStream实现SSR,组件内通过Apollo Client获取数据。为了让客户端重水合服务端缓存,需要把缓存提取出来,通过服务器发送的HTML内联脚本暴露到全局window对象上。
目前我通过手动跟踪请求的方式实现了功能:
- 用
wrapPromise包装请求,未完成时抛出Promise触发Suspense挂起
export function wrapPromise<T>(promise: Promise<T>): () => T { let status = 'pending'; let response: T; const suspender = promise.then( (res) => { status = 'success'; response = res; }, (err) => { status = 'error'; response = err; } ); return () => { switch (status) { case 'pending': throw suspender; case 'error': throw response; default: return response; } }; }
- 编写
ApolloSSRCache组件,手动遍历所有包装后的请求触发执行,待所有请求完成后渲染缓存脚本
export const ApolloSSRCache = () => { Object.values(renderCache).forEach((wrapped) => wrapped()); return ( <script dangerouslySetInnerHTML={{ __html: `window.__APOLLO_STATE__=${JSON.stringify( client.extract() ).replace(/</g, '\\u003c')};`, }} /> ); };
这个方案能正常工作,客户端useQuery可直接复用缓存,避免重复请求和页面闪烁,但手动跟踪请求的方式过于繁琐。我想知道有没有**兼容Suspense(用于代码分割)**的流式渲染下缓存暴露与重水合的标准方案?
我的更新发现
- 更新1:如果服务端使用
useSuspenseQuery,把暴露缓存的脚本标签放在渲染内容下方,缓存会被正确填充——因为执行查询的组件会自动挂起,client.extract()会在所有请求完成后调用,无需手动挂起标签。 - 更新2:但
useSuspenseQuery会忽略{fetchPolicy: 'network-only'},SSR期间会一直返回缓存数据,直到服务器重启。
标准解决方案(兼容流式SSR与Suspense)
结合Apollo Client的Suspense原生支持和React 18的流式渲染能力,推荐以下优化方案:
1. 基于useSuspenseQuery的自动缓存收集
完全利用Apollo和React的原生能力,无需手动跟踪请求:
- 服务端组件统一使用
useSuspenseQuery发起请求,组件会在请求未完成时自动触发Suspense挂起 - 将提取缓存的脚本标签放在SSR渲染的根内容之后(比如
</body>前),React流式渲染会等待所有Suspense边界内的请求完成后,再渲染该脚本标签,此时client.extract()就能拿到完整的缓存数据
示例服务端渲染逻辑:
// 服务端创建独立的Apollo Client实例(避免跨请求缓存污染) const apolloClient = createApolloClient({ ssrMode: true }); const { pipe } = renderToPipeableStream( <ApolloProvider client={apolloClient}> <Suspense fallback={<div>加载中...</div>}> <App /> </Suspense> {/* 放在根内容之后,自动等待所有请求完成 */} <script dangerouslySetInnerHTML={{ __html: `window.__APOLLO_STATE__=${JSON.stringify( apolloClient.extract() ).replace(/</g, '\\u003c')};`, }} /> </ApolloProvider>, { onShellReady() { // 流式输出HTML外壳 pipe(res); }, onAllReady() { // 所有内容渲染完成后的回调 }, } );
2. 解决useSuspenseQuery忽略network-only的问题
针对SSR时useSuspenseQuery忽略fetchPolicy: 'network-only'的问题,可通过两种方式处理:
- 每次请求创建新的Apollo Client实例:服务端处理每个请求时都初始化全新的Client,避免跨请求复用缓存,确保每次SSR都发起新的网络请求
- 手动预取数据并写入缓存:在渲染前手动执行查询并指定
network-only,将结果写入缓存后再开始渲染
示例手动预取逻辑:
// 服务端预取关键数据 const prefetchCriticalData = async (client: ApolloClient<NormalizedCacheObject>) => { await client.query({ query: YOUR_CRITICAL_QUERY, fetchPolicy: 'network-only', }); }; // 渲染前先预取数据 await prefetchCriticalData(apolloClient); // 再执行流式渲染 const { pipe } = renderToPipeableStream(/* ... */);
3. 客户端重水合逻辑
客户端初始化Apollo Client时,读取全局的window.__APOLLO_STATE__并恢复缓存:
const client = createApolloClient({ ssrMode: false, cache: new InMemoryCache().restore(window.__APOLLO_STATE__), }); ReactDOM.hydrateRoot( document.getElementById('root'), <ApolloProvider client={client}> <App /> </ApolloProvider> );
核心优势
- 无需手动跟踪请求/Promise,完全依赖框架原生能力
- 完美兼容React 18流式渲染和代码分割的Suspense边界
- 解决缓存复用问题,确保SSR每次都能获取最新数据
内容的提问来源于stack exchange,提问作者Moritz Roessler
相关产品推荐
相关产品推荐

