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

使用getServerSideProps时Hydration失败:Urql与next-i18next SSR问题排查

问题排查:Urql SSR与next-i18next结合的Hydration错误

问题背景

我使用Urql客户端向GraphQL服务器发起查询,初始代码运行正常:

const username = () => {
  const { t: translation } = useTranslation(["common", "profile"]);

  const { query, locale } = useRouter();
  const username = query.username;
  const [{ data: userProfileData }] = useUserProfileQuery({
    variables: {
      username: username as string,
    },
    pause: isServer || !username,
  });
  const userProfile = userProfileData?.userProfile;

  return (
    <Text>{userProfile.name}</Text>
  );
};

export default withUrqlClient(createUrqlClient, { ssr: false })(username);

为了用next-i18next实现国际化,必须添加getServerSideProps,但这导致无法通过ssr: true开启服务端渲染,我的目标是让useUserProfileQuery在服务端执行查询。

按照Urql SSR指南修改getServerSideProps后:

export const getServerSideProps = async ({ locale, locales, query }: any) => {
  const ssrCache = ssrExchange({ isClient: false });
  const client = initUrqlClient(createUrqlClient(ssrCache), false);
  if (client)
    await client
      .query(UserProfileDocument, {
        username: query.username,
      })
      .toPromise();

  return {
    props: {
      ...(await serverSideTranslations(
        locale,
        ["common", "profile", "settings"],
        null,
        locales
      )),
      urqlState: ssrCache.extractData(),
    },
  };
};

出现以下Hydration错误:

Error: Hydration failed because the initial UI does not match what was rendered on the server.

Warning: Expected server HTML to contain a matching <div> in <div>

错误原因

  1. 服务端与客户端数据不同步:服务端通过initUrqlClient预取了数据,但客户端未利用urqlState初始化缓存,导致初始渲染无数据,和服务端HTML输出不一致。
  2. pause参数逻辑错误:原代码中pause: isServer || !username导致服务端渲染组件时查询被暂停,无法读取预取的缓存数据,服务端输出空内容,客户端hydration时拿到数据后渲染出内容,引发不匹配。
  3. 未处理空数据情况:直接访问userProfile.name,若服务端预取失败或客户端初始无数据,会导致渲染内容差异。

修复步骤

1. 开启withUrqlClient的SSR模式

将ssr: false改为ssr: true,让客户端自动读取服务端传来的urqlState初始化缓存:

export default withUrqlClient(createUrqlClient, { ssr: true })(username);

2. 调整pause条件

服务端已预取数据,组件渲染时无需暂停查询,仅在客户端无username时暂停:

const [{ data: userProfileData }] = useUserProfileQuery({
  variables: {
    username: username as string,
  },
  pause: !isServer && !username, // 服务端不暂停,客户端无username时暂停
});

3. 为空数据添加兜底

避免因userProfile为undefined导致渲染内容差异:

return (
  <Text>{userProfile?.name || ''}</Text>
);

4. 确保createUrqlClient正确集成ssrExchange

检查createUrqlClient函数是否将传入的ssrCache加入exchanges数组:

export const createUrqlClient = (ssrExchange?: any) => ({
  url: '你的GraphQL服务地址',
  exchanges: [
    dedupExchange,
    cacheExchange,
    ...(ssrExchange ? [ssrExchange] : []),
    fetchExchange,
  ],
});

5. 优化getServerSideProps的查询逻辑

确保仅在存在username时执行查询,避免无效请求:

export const getServerSideProps = async ({ locale, locales, query }: any) => {
  const ssrCache = ssrExchange({ isClient: false });
  const client = initUrqlClient(createUrqlClient(ssrCache), false);
  
  if (client && query.username) {
    await client
      .query(UserProfileDocument, {
        username: query.username,
      })
      .toPromise();
  }

  return {
    props: {
      ...(await serverSideTranslations(
        locale,
        ["common", "profile", "settings"],
        null,
        locales
      )),
      urqlState: ssrCache.extractData(),
    },
  };
};

内容的提问来源于stack exchange,提问作者jmecs

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 21:02:08