巨坑!服务端渲染中,时间处理不当带来的水合错误

巨大的坑

写在前面

水合错误(Hydration Error)是 React 在客户端「水合」服务器渲染 HTML 时,发现服务器生成的 HTML 与客户端渲染的结果不一致,从而抛出的错误或警告。

📌 什么是“水合”?

在 SSR(Server Side Rendering)中,React 首先在服务器上生成 HTML,再发送到浏览器,浏览器接收到 HTML 后会“水合(hydrate)”——也就是把 React 的事件和状态逻辑绑定到已有的 HTML 上,形成一个可交互的 React 应用。

如果 HTML 和客户端生成的内容不一致,就会报错。

❗水合错误的常见原因

✅ 1. 使用了浏览器环境特有对象(如 window, document)

服务端是 Node.js,没有浏览器环境,这些对象是 undefined:

// ❌ 错误:这会在 SSR 阶段就执行
const width = window.innerWidth

应该这样写:

// ✅ 正确:只在客户端执行
useEffect(() => {
  const width = window.innerWidth
}, [])
✅ 2. 首次渲染时,数据不一致(如时间戳、随机数)
// ❌ 错误:每次 SSR 和 CSR 结果不一样
const now = Date.now() // 会导致 HTML 内容不同

解决方式:

// ✅ 正确:延后到 useEffect 里执行
const [now, setNow] = useState(0)
useEffect(() => {
  setNow(Date.now())
}, [])
✅ 3. 组件内部条件渲染逻辑服务端/客户端结果不同

例如依赖 localStorage 或用户登录状态:

// ❌ localStorage 在 SSR 中不存在
const isLoggedIn = localStorage.getItem('token')

解决方式:

const [isClient, setIsClient] = useState(false)
useEffect(() => setIsClient(true), [])

return isClient ? <LoggedInUI /> : null
✅ 4. 使用了某些只支持客户端的第三方库(如图表、动画库)

很多 UI 库依赖浏览器 API,如果直接 SSR 会失败。可以用:

const Chart = dynamic(() => import('./Chart'), { ssr: false })
做法 是否推荐 说明
useEffect 中访问浏览器 API ✅ 推荐 避免在 SSR 阶段运行
使用 typeof window !== 'undefined' 判断环境 ✅ 推荐 确保客户端执行
避免初始渲染用随机数/时间戳等 ✅ 推荐 SSR/CSR 结果应一致
使用 dynamic(..., { ssr: false }) 仅在客户端加载组件 ✅ 有用,但注意 SEO 影响

巨坑:时间格式化带来的水合问题

上面介绍了几种水合问题和解决方式,但实际应用中,还是会有一些坑,让人防不胜防
比如:

同样的时间戳1752328800,为什么会出现在服务端返回的html中是2025-07-12 14:00:00;而客户端中是2025-07-12 22:00:00

这是一个经典的时区不一致问题 —— 服务端和客户端对同一个时间戳的解析结果不一致,导致你看到:

  • 服务端返回 HTML 中时间是:2025-07-12 14:00:00

  • 客户端渲染时间却是:2025-07-12 22:00:00

解释

服务端(Node.js)默认使用 UTC 时区

在服务器中(比如 Vercel、Next.js 的 getServerSideProps),你写:

new Date(1752328800 * 1000).toLocaleString()
// → "2025-07-12 14:00:00"(UTC)
客户端(浏览器)使用 用户本地时区

如果你人在日本(UTC+9),浏览器会默认用你的系统时区:

new Date(1752328800 * 1000).toLocaleString()
// → "2025-07-12 23:00:00"(UTC+9)

你看到的是 22:00:00,可能是因为浏览器或系统设成了 UTC+8。

所以直接在JSX中使用dayjs.unix(Number(1752328800)).format('YYYY-MM-DD HH:mm:ss')的方式进行格式化展示,就会造成水合错误

解决方案:
export function useLocalTime(timestamp: number, format = 'YYYY-MM-DD HH:mm:ss') {
  const [value, setValue] = useState('')

  useEffect(() => {
    setValue(dayjs.unix(timestamp).format(format))
  }, [timestamp, format])

  return value
}
const formatted = useLocalTime(1752328800)
return <span>{formatted}</span>

也就是说使用useEffect,确保是客户端才进行访问

小坑,直接使用new Date()

在代码中直接使用new Date(),会导致客户端取值和服务端取值不一致,导致水合问题。
同样可以放到useEffect中解决

总结

👉 一切包含“浏览器环境依赖(如 Date、window、时区)”的代码,都应该放在 useEffect 中或客户端-only组件中执行

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • """1.个性化消息: 将用户的姓名存到一个变量中,并向该用户显示一条消息。显示的消息应非常简单,如“Hello ...
    她即我命阅读 3,284评论 0 5
  • 为了让我有一个更快速、更精彩、更辉煌的成长,我将开始这段刻骨铭心的自我蜕变之旅!从今天开始,我将每天坚持阅...
    李薇帆阅读 1,912评论 0 3
  • 似乎最近一直都在路上,每次出来走的时候感受都会很不一样。 1、感恩一直遇到好心人,很幸运。在路上总是...
    时间里的花Lily阅读 1,355评论 0 1
  • 1、expected an indented block 冒号后面是要写上一定的内容的(新手容易遗忘这一点); 缩...
    庵下桃花仙阅读 517评论 0 1
  • 一、工具箱(多种工具共用一个快捷键的可同时按【Shift】加此快捷键选取)矩形、椭圆选框工具 【M】移动工具 【V...
    墨雅丫阅读 533评论 0 0