Note: 本文並不包含 什麼是 Hydration 。
問題
如果你有寫過 Next.js 或是任意的 SSR, SSG 方案 ( 包含 vue ),
你可能有遇過下圖的錯誤訊息。
或是
這類型錯誤被稱作 Hydration Mismatch Error,
這也是很多工程師從純粹 Client-Side Render ( 像是 CRA ) 轉戰 Server-Side Render 遇到的其中一個痛點。
即便他暫時沒有影響應用程式的正常運作,也千萬不要無視這個問題,
尤其在真正的產品開發專案,用到 SSR 的機會真的很多,
不要等到產生了無法預期的錯誤才處理,
建議遇到的當下就要立即處理這個問題,
不然專案複雜度隨著時間倍增,
解決問題的機會也會越來越渺茫。
雖然目前沒有很有效的方案來避免遇到它,
但他的本質其實非常單純,
只要釐清問題發生的根本原因,就可以找出很多種方案處理它。
以下就來說明問題的發生原因。
根本原因
在 Server 端預先渲染 React,其結果是純粹的 HTML 字串,
當 Client 端接收到預渲染的 HTML 並會在 React 執行 第一次 渲染後,
將兩者的產出結果 (React Tree 跟 DOM Tree) 做比對,當有一處不吻合時就會爆出 Hydration Mismatch Error ,
也就是說 第一次渲染的結果,一定要跟 Server 預渲染的 HTML 一模一樣。
這個其實是一個好的防錯機制,因為假設兩者產生的結果不一樣,
也可能會影響 Hook
的執行順序也會不一樣 (Hook
是根據 Component
的呼叫順序排序),
進而導致運行階段的邏輯錯誤,如果此時涉及到真實世界的交易行為,那就不是簡單的事件了。
程式碼出錯的可能原因
window 或 document
當某段元件程式碼使用到 window
或是 document
等 DOM API 時,
為了避免 Server 噴錯 (aka window is not defined
)。
設計面
當元件設計在 Mobile 跟 Desktop 兩個版型的結構差距太大時,
通常就會採用 Conditional Display 的方案處理,
也就是跟據 media query 判斷顯示哪個版型的介面。
可能的解決方案
因為解決方案會根據使用情境下去做變化,
以下提出幾個常見的案例作為參考,
並針對其案例提出我認為適合的解法。
Responsive Modal
有些元件,其並非會影響到資訊結構跟版型時,就可以透過第一個解法 ClientOnly 解決。
以下為示範程式碼:
function useClientOnly() {
const [hasFirstRendered, setFirstRendered] = useState(false)
useEffect(() => {
setFirstRendered(true)
}, [setFirstRendered])
return hasFirstRendered
}
function ClientOnlyModal() {
const isClient = useClientOnly()
if (!isClient) return null
return <Modal />
}
因為 ClientOnly 延後了 Modal 的渲染,
第一次渲染的比對皆是 null
,所以不會發生錯誤。
Responsive Header
有些工程師會直覺用在 React 程式下判斷 media query 並根據條件回傳 React Element,
function Header() {
if (window.matchMedia('(min-width: 600px)')) return <Mobile />
return <Desktop />
}
這個在 Client Side Render 可以正常運作,
但在 Server Side Render 就會噴出 Mismatch Error
。
你可能可以用上面提到的 ClientOnly
來解決,
function Header() {
const isClient = useClientOnly()
if (!isClient) return null
if (window.matchMedia('(min-width: 600px)')) return <Mobile />
return <Desktop />
}
雖然可以規避掉 第一次渲染的問題,
但會造成 Layout Shift ( aka 大範圍位移 ),
且也可能會影響 SEO,因為通常 Header 裡面有網站的主要功能入口 ( Navigation ),
這裡的建議解法是 把兩個元件都渲染上去,透過 CSS 控制顯示。
.hidden {
display: hidden;
}
.block {
display: block;
}
@media (min-width: 600px) {
.md\:hidden {
display: hidden;
}
.md\:block {
display: block;
}
}
function Header() {
return (
<>
<Mobile className="block md:hidden" />
<Desktop className="hidden md:block" />
</>
)
}
這個做法不會造成 Layout Shift,
原因是 CSS 的 Layout 計算時間發生在 First Paint 之前 see,
也不會影響到 SEO 因為在 Server 預渲染時資訊都有被選染出來。
總結
當遇到 Hydration Mismatch Error 時,
就是指 第一次渲染時,Server 預渲染的 HTML 與 Client 端 React 結果不吻合,
所以要尋找是哪一段造成兩邊不吻合。
找到不吻合之處之後,在根據該元件的 用途 下去決定要怎麼處理。