祝你好运的技术博客

Published on

理解和修复React水合问题

Authors
  • avatar
    Name
    祝你好运
    Twitter

1. 渲染条件错误

我们项目是Next.js + CDN的架构设计,尤其是导航条里面用户信息的展示。

这里就是有水合问题的,水合问题说的是,React在客户端第一次渲染后的结果跟SSR出来的结果不一致。而带来这个问题的原因不是这种方案不行,而是我们的实现方式不对,我们的实现代码类似下面的:

const isSSR = typeof window === 'undefined'

// 如果有认证信息,在用户信息确定之前(query 还未完成),应该一直显示骨架屏
// clientUser === undefined 表示 query 还没有结果(可能是 enabled 为 false,或者还在加载)
// 只有当 query 完成(isFetched 为 true)后,才显示真实内容
// 如果没有认证信息,直接显示未登录态
const shouldShowSkeleton = isSSR || (hasAuth && (!isFetched || isUserLoading || !user))

这里的问题就在于,服务端渲染的时候,isSSR是true,所以shouldShowSkeleton是true。然后客户端渲染的时候,如果未登录那hasAuth就是false,然后shouldShowSkeleton也是false。那这就是水合问题。如何解决呢?用下面的代码就好了:

const [hasMounted, setHasMounted] = useState(false)

useEffect(() => {
  setHasMounted(true)
}, [])

// hydration 完成前始终显示骨架屏,与 CDN/SSR 缓存的 HTML 保持一致
// mount 后再根据 cookie / user 状态切换为真实 UI
const shouldShowSkeleton = !hasMounted || (hasAuth && (!isFetched || isUserLoading || !user))

2. 嵌套p标签

我们有下面的代码:

<div
  ref={contentRef}
  className="md:pl-21 font-inter pl-[42px] text-xs font-normal leading-[1.6] text-white/60 md:text-xl"
  dangerouslySetInnerHTML={{
    __html: question.answer || '',
  }}
/>

而这个question.answer是来自动态运行时的i18n服务,那里面确实是有的用了<p>。当远程 answer 里含有<p>,再套在外层<p>上:

<!-- React 想渲染的结构 -->
<p class="text-xs...">
  <p class="mb-4">1. Shop on the Store page...</p>
  <p class="mb-4">2. Recharge your balance...</p>
  ...
</p>

HTML 规范不允许 <p> 嵌套 <p>,浏览器解析时会自动「拆」。React hydration 时期望的是第一种结构,实际 DOM 是第二种,于是就报错了。报错里的:

  • __html: "" → 外层 <p>被掏空
  • 多个 <p class="mb-4"> → 内层段落被提升成兄弟节点
<!-- 浏览器实际 DOM -->
<p class="text-xs..."></p>
<!-- 外层变空 -->
<p class="mb-4">1. Shop...</p>
<!-- 被提升为兄弟节点 -->
<p class="mb-4">2. Recharge...</p>