NextJS 的 hydration errors 是什麼,為什麼會發生我又該如何解決

想搞懂為什麼你的 Next.js 專案總是出現 react-hydration-error 嗎?這篇文章深入解析 Hydration 的原理,帶你了解 Next.js 的 SSR 與 CSR 流程,並提供三種避免 Hydration Error 的實用解法,讓你的 React 頁面更穩定、無錯誤

CChia1104
文章5 分鐘閱讀

nextjs hydration errors

過去大家在開發 NextJS 的專案時一定看過這類錯誤 react-hydration-error,在說明這問題如何解決前我們要先了解 NextJS 是如何做 SSR (Server-Side Rendering) 跟 CSR (Client-Side Rendering) 的。

NextJS 做 CSR ?

我們都知道 NextJS 是一個 React 的全端框架,用 React 的方式做到 SSR。

但 NextJS 跟傳統全端框架 - 如:PHP 的 Laravel 比較不一樣的點是 他只會在使用者第一次進到的頁面 預生成 HTML 來做 SSR,後續我們再做其他頁面的導覽時就跟傳統 SPA (Single-Page Application) 的 CSR 概念一樣,只把我們需要的元件載入進來以防止整個頁面的重載 - NextJS 用戶端導頁

React SSR

React 在伺服器端有對應的 API 來生成 HTML,其中一個是 renderToPipeableStream

此方法的調用可以使 React 逐步生成 HTML 並透過 Node.js Stream,將 HTML 串流到客戶端。

import { renderToPipeableStream } from 'react-dom/server';

const { pipe } = renderToPipeableStream(<App />, {
  bootstrapScripts: ['/main.js'],
  onShellReady() {
    response.setHeader('content-type', 'text/html');
    pipe(response);
  }
});

流程大概如下:

  1. 伺服器端的渲染 (SSR):React 在 伺服器端逐步生成 HTML。
  2. Streaming HTML:當部分 HTML 完成時,立刻串流到客戶端。
  3. 瀏覽器接收並渲染 HTML:瀏覽器開始接收 HTML 後並載入 JavaScript 資源。
  4. Hydration:React 將靜態的 HTML 轉換為可以互動的 React 元件,這個過程稱為 Hydration。

如何 Hydration

剛剛說第一次渲染的是 HTML 那他要怎麼跟 React 互動,又換一個方式說:我們要怎麼透過這個 HTML 讓我們有辦法透過 React 的 API 如 useState 來做互動。

hydrateRoot

在 Server 端完成 SSR,將靜態 HTML 傳送至客戶端後,React 可以透過 hydrateRoot 將這些靜態的 HTML 轉換為可互動的 React 元件。

const root = hydrateRoot(domNode, reactNode)
  • domNode: 對應到 HTML 當中的跟節點元件 (SSR 預生成的 HTML)。
  • reactNode: 為生成對應 HTML 的 React 元件。
import { hydrateRoot } from "react-dom/client";
import App from "./App";

hydrateRoot(document.getElementById("root"), <App />);

這個將伺服器預先渲染好的靜態 HTML,與客戶端的 React 應用程式結合,並賦予其互動性的過程,就稱為 Hydration

伺服器跟客戶端都會各渲染一次

hydrateRoot 這方法來看,這是在客戶端做畫面渲染所引用的,而 NextJS 在做 SSR pre-render 時也會用 renderToPipeableStream 做 HTML 預生成。

hydrateRoot 的預期內容必須與預渲染的 HTML 完全一致,不然就會導致 react-hydration-error

Hydration Error

從剛才的流程來看,React 為了確保伺服器跟客戶端所生成的 HTML 一樣,在 hydrateRoot 的時候若有不一樣就會產生 Hydration Error。

通常最常見的 Hydration Error 大概就是時間的處理了,例如時區:通常伺服器端的時區多半是 +0 的 UTC 時區,但使用者端一定是跟者瀏覽器的時區走。

NextJS TZ Hydration Error

除了時間之外,若是我們直接用瀏覽器端的 API 如 localStorage 等也會造成 Hydration Error,因為伺服器端沒有 localStorage

解決辦法

  1. useEffect 判斷伺服器或客戶端
"use client";

import { useState, useEffect } from 'react'
 
export default function App() {
  const [isClient, setIsClient] = useState(false)
 
  useEffect(() => {
    setIsClient(true)
  }, [])
 
  return <h1>{isClient ? 'This is never prerendered' : 'Prerendered'}</h1>
}

因為 useEffect 是基於瀏覽器的環境運行的,在伺服器端及客戶端的 DOM tree 中的初始值 (isClient) 永遠是 false,進而避免 Hydration Error。

主要確保伺服器端及客戶端的初始值一樣即可。

  1. NextJS 的 dynamic SSR 為 false

可以透過 NextJS 的 dynamic API 來標注哪個元件不需要做 pre-render,在伺服器跟客戶端的 hydrateRoot 都不做生成。

import dynamic from 'next/dynamic'
 
const NoSSR = dynamic(() => import('../components/no-ssr'), { ssr: false })
 
export default function Page() {
  return (
    <div>
      <NoSSR />
    </div>
  )
}
  1. 使用 suppressHydrationWarning

該方法會取消 Hydration Error 的警告,過去在用 next-themes 這套件的時候應該會設定到,如果我們會需要客戶端動態調整某個 DOM 元件的參數,由於伺服器端沒這個值進而導致的 Hydration Error,就可以用該方法來靜音。

<html colorSchema="dark" suppressHydrationWarning />

Written by: Chia1104 CC BY-NC-SA 4.0

Chia1104
©
Chia1104