使用Apollo Client useQuery在Next.js中出现Hydration错误的问题
问题背景
在Next.js 12.1.6(Pages路由)搭配Apollo Client 3.3.19时,使用useQuery会触发Hydration错误:SSR阶段loading状态为false(服务端已获取数据并渲染目标组件),但客户端初始渲染时loading为true,导致服务端与客户端生成的HTML结构不匹配,触发Hydration冲突。目前用useEffect+useLazyQuery能临时解决,但需要更优方案。
问题代码示例:
// 服务端渲染组件,客户端初始渲染却显示"Loading",触发Hydration错误 function Page () { const {data, loading} = useQuery(query) if(loading) { return "Loading" } return <Component data={data} /> }
依赖版本:
@apollo/client : 3.3.19 next: 12.1.6 (pages router)
核心原因
Apollo Client在SSR时会预取数据并存入缓存,但客户端初始化阶段,服务端传递的缓存数据还未完成同步(即客户端缓存未完成Hydration),此时useQuery会判定数据未加载,loading状态为true,和服务端的渲染结果产生冲突。
最优解决方案
方法1:配置Apollo Client的ssrForceFetchDelay
在初始化Apollo Client时添加ssrForceFetchDelay选项,让客户端在初始渲染时短暂等待,确保服务端缓存同步完成,避免loading状态不一致。
示例代码:
// apollo-client.js import { ApolloClient, InMemoryCache } from '@apollo/client'; const client = new ApolloClient({ uri: '/api/graphql', cache: new InMemoryCache(), ssrMode: typeof window === 'undefined', // 服务端启用SSR模式 ssrForceFetchDelay: 100, // 延迟客户端初始查询,等待缓存同步 }); export default client;
这个配置会让客户端在初始渲染时延迟指定时间再执行查询,确保服务端传递的缓存数据已同步到客户端,此时loading状态会和服务端保持一致。
方法2:通过getServerSideProps预取数据并传递缓存
在页面的getServerSideProps中手动用Apollo Client查询数据,将缓存状态传递给页面组件,组件直接复用缓存数据,避免在组件内调用useQuery导致的状态差异。
示例代码:
// pages/[page].js import { initializeApollo } from '../apollo-client'; import { query } from '../queries'; export async function getServerSideProps(context) { const apolloClient = initializeApollo(); await apolloClient.query({ query }); return { props: { initialApolloState: apolloClient.cache.extract(), // 传递服务端缓存状态给客户端 }, }; } function Page({ initialApolloState }) { // 初始化客户端时注入服务端缓存 const client = initializeApollo(initialApolloState); const { data } = client.readQuery({ query }); // 直接从缓存读取数据,无loading状态 return <Component data={data} />; } export default Page;
原理是通过getServerSideProps在服务端完成数据查询,将缓存状态传递给客户端,客户端初始化时直接复用缓存数据,组件内无需等待loading,服务端和客户端渲染结果完全一致。
方法3:用useEffect标记客户端Hydration完成
如果不想修改全局配置或服务端逻辑,可以在组件内用useEffect标记客户端是否完成初始Hydration,仅在客户端完成Hydration后才显示loading,避免服务端和客户端的渲染差异。
示例代码:
import { useState, useEffect } from 'react'; import { useQuery } from '@apollo/client'; function Page() { const [isClientHydrated, setIsClientHydrated] = useState(false); const { data, loading } = useQuery(query); useEffect(() => { // useEffect仅在客户端执行,标记Hydration完成 setIsClientHydrated(true); }, []); // 服务端渲染时跳过loading分支,客户端Hydrated后再处理loading状态 if (isClientHydrated && loading) { return "Loading"; } return <Component data={data} />; }
这个方案利用useEffect只在客户端执行的特性,确保服务端渲染时不会进入loading分支,客户端完成Hydration后再处理loading状态,避免HTML结构不一致。
临时方案对比
之前用useEffect+useLazyQuery的方式需要手动触发查询,代码冗余且可能影响用户体验;上述方案更简洁,且能从根源上解决状态不一致问题。
内容的提问来源于stack exchange,提问作者sujan basnet

