React Pattern:當遇到 Hydration Mismatch 的處理方案

Photo by 傅甬 华 on Unsplash

React Pattern:當遇到 Hydration Mismatch 的處理方案

Note: 本文並不包含 什麼是 Hydration

問題

如果你有寫過 Next.js 或是任意的 SSR, SSG 方案 ( 包含 vue ),
你可能有遇過下圖的錯誤訊息。 Error1 或是 Error2

這類型錯誤被稱作 Hydration Mismatch Error
這也是很多工程師從純粹 Client-Side Render ( 像是 CRA ) 轉戰 Server-Side Render 遇到的其中一個痛點。

即便他暫時沒有影響應用程式的正常運作,也千萬不要無視這個問題,
尤其在真正的產品開發專案,用到 SSR 的機會真的很多,
不要等到產生了無法預期的錯誤才處理,
建議遇到的當下就要立即處理這個問題,
不然專案複雜度隨著時間倍增, 解決問題的機會也會越來越渺茫。

雖然目前沒有很有效的方案來避免遇到它,
但他的本質其實非常單純,
只要釐清問題發生的根本原因,就可以找出很多種方案處理它。

以下就來說明問題的發生原因。

根本原因

Server 端預先渲染 React,其結果是純粹的 HTML 字串,
Client 端接收到預渲染的 HTML 並會在 React 執行 第一次 渲染後,
將兩者的產出結果 (React TreeDOM Tree) 做比對,當有一處不吻合時就會爆出 Hydration Mismatch Error
也就是說 第一次渲染的結果,一定要跟 Server 預渲染的 HTML 一模一樣

這個其實是一個好的防錯機制,因為假設兩者產生的結果不一樣,
也可能會影響 Hook 的執行順序也會不一樣 (Hook 是根據 Component 的呼叫順序排序),
進而導致運行階段的邏輯錯誤,如果此時涉及到真實世界的交易行為,那就不是簡單的事件了。

程式碼出錯的可能原因

window 或 document

當某段元件程式碼使用到 window 或是 documentDOM API 時, 為了避免 Server 噴錯 (aka window is not defined )。

設計面

當元件設計在 MobileDesktop 兩個版型的結構差距太大時,
通常就會採用 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 結果不吻合
所以要尋找是哪一段造成兩邊不吻合。

找到不吻合之處之後,在根據該元件的 用途 下去決定要怎麼處理。

Did you find this article valuable?

Support Return To Dream Land by becoming a sponsor. Any amount is appreciated!