写在前面
水合错误(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组件中执行