
寫在前面
水合錯誤(Hydration Error)是 React 在客戶端「水合」服務器渲染 HTML 時,發(fā)現(xiàn)服務器生成的 HTML 與客戶端渲染的結(jié)果不一致,從而拋出的錯誤或警告。
?? 什么是“水合”?
在 SSR(Server Side Rendering)中,React 首先在服務器上生成 HTML,再發(fā)送到瀏覽器,瀏覽器接收到 HTML 后會“水合(hydrate)”——也就是把 React 的事件和狀態(tài)邏輯綁定到已有的 HTML 上,形成一個可交互的 React 應用。
如果 HTML 和客戶端生成的內(nèi)容不一致,就會報錯。
?水合錯誤的常見原因
? 1. 使用了瀏覽器環(huán)境特有對象(如 window, document)
服務端是 Node.js,沒有瀏覽器環(huán)境,這些對象是 undefined:
// ? 錯誤:這會在 SSR 階段就執(zhí)行
const width = window.innerWidth
應該這樣寫:
// ? 正確:只在客戶端執(zhí)行
useEffect(() => {
const width = window.innerWidth
}, [])
? 2. 首次渲染時,數(shù)據(jù)不一致(如時間戳、隨機數(shù))
// ? 錯誤:每次 SSR 和 CSR 結(jié)果不一樣
const now = Date.now() // 會導致 HTML 內(nèi)容不同
解決方式:
// ? 正確:延后到 useEffect 里執(zhí)行
const [now, setNow] = useState(0)
useEffect(() => {
setNow(Date.now())
}, [])
? 3. 組件內(nèi)部條件渲染邏輯服務端/客戶端結(jié)果不同
例如依賴 localStorage 或用戶登錄狀態(tài):
// ? localStorage 在 SSR 中不存在
const isLoggedIn = localStorage.getItem('token')
解決方式:
const [isClient, setIsClient] = useState(false)
useEffect(() => setIsClient(true), [])
return isClient ? <LoggedInUI /> : null
? 4. 使用了某些只支持客戶端的第三方庫(如圖表、動畫庫)
很多 UI 庫依賴瀏覽器 API,如果直接 SSR 會失敗??梢杂茫?/p>
const Chart = dynamic(() => import('./Chart'), { ssr: false })
| 做法 | 是否推薦 | 說明 |
|---|---|---|
在 useEffect 中訪問瀏覽器 API |
? 推薦 | 避免在 SSR 階段運行 |
使用 typeof window !== 'undefined' 判斷環(huán)境 |
? 推薦 | 確保客戶端執(zhí)行 |
| 避免初始渲染用隨機數(shù)/時間戳等 | ? 推薦 | SSR/CSR 結(jié)果應一致 |
使用 dynamic(..., { ssr: false }) 僅在客戶端加載組件 |
? 有用,但注意 SEO 影響 |
巨坑:時間格式化帶來的水合問題
上面介紹了幾種水合問題和解決方式,但實際應用中,還是會有一些坑,讓人防不勝防
比如:
同樣的時間戳1752328800,為什么會出現(xiàn)在服務端返回的html中是2025-07-12 14:00:00;而客戶端中是2025-07-12 22:00:00
這是一個經(jīng)典的時區(qū)不一致問題 —— 服務端和客戶端對同一個時間戳的解析結(jié)果不一致,導致你看到:
服務端返回 HTML 中時間是:
2025-07-12 14:00:00客戶端渲染時間卻是:
2025-07-12 22:00:00
解釋
服務端(Node.js)默認使用 UTC 時區(qū)
在服務器中(比如 Vercel、Next.js 的 getServerSideProps),你寫:
new Date(1752328800 * 1000).toLocaleString()
// → "2025-07-12 14:00:00"(UTC)
客戶端(瀏覽器)使用 用戶本地時區(qū)
如果你人在日本(UTC+9),瀏覽器會默認用你的系統(tǒng)時區(qū):
new Date(1752328800 * 1000).toLocaleString()
// → "2025-07-12 23:00:00"(UTC+9)
你看到的是 22:00:00,可能是因為瀏覽器或系統(tǒng)設成了 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中解決
總結(jié)
?? 一切包含“瀏覽器環(huán)境依賴(如 Date、window、時區(qū))”的代碼,都應該放在 useEffect 中或客戶端-only組件中執(zhí)行