Hydration Errorと戦う


Next.jsを使っていて、突如として現れる謎のHydration Errorに悩まされたことはありませんか？
このエラーは開発者を悩ませる厄介な問題の一つですが、実は理解して対処すれば、そこまで怖くない相手なんです。
今回は、このHydration Errorについて、その正体から対処法まで、具体的なコード例を交えながら紐解いていきましょう。

## Hydration Errorとは？
まず、Hydration Errorの正体について説明しましょう。
このエラーは、サーバーサイドでレンダリングされたHTMLと、クライアントサイドでレンダリングされたコンポーネントの内容が一致しない時に発生します。
簡単に言えば、サーバーとクライアントで「見た目が違う！」と言って喧嘩を始めてしまうようなものです。

## エラーが発生するまでの流れ
Hydration Errorが発生するまでの過程を詳しく見ていきましょう。

**1. サーバーサイドレンダリング（SSR）**
ユーザーがページをリクエストすると、Next.jsはサーバー上でReactコンポーネントをレンダリングします。
この時点で、useStateの初期値やgetServerSidePropsからのデータを使用してHTMLが生成されます。


**2. 初期HTMLの送信**
生成されたHTMLがクライアントに送信されます。
ユーザーは瞬時にコンテンツを見ることができます（これがSSRの利点です）。


**3. JavaScriptの読み込みと実行**
ブラウザがHTMLを受け取った後、リンクされたJavaScriptファイルをダウンロードし実行します。
この段階で、Reactアプリケーションがクライアントサイドでhydrationプロセスを開始します。


**4. Hydrationプロセス**
Reactは既存のDOMノードを利用し、イベントリスナーを付加します。
この過程で、Reactは仮想DOMを構築し、実際のDOMと比較します。


**5. 不一致の検出**
もし仮想DOMと実際のDOMの内容が一致しない場合、Reactはこれを検出します。
この不一致がHydration Errorとしてコンソールに報告されます。

## Hydration Errorの主な原因（コード例付き）
**1. クライアントサイド専用APIの不適切な使用**
`window`, `document`, `localStorage`などのブラウザ専用APIをコンポーネントの初期レンダリング時に使用すると、サーバーサイドでは動作しないため不一致が発生します。
```react
// 問題のあるコード
function WindowSize() {
  // このコードはサーバーサイドで実行されるとエラーになります
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return <div>Window width: {width}px</div>;
}
```

**2. 動的なデータの扱いの問題**
サーバーサイドとクライアントサイドで異なる値を持つ可能性のあるデータ（現在時刻、ランダムな値など）を直接レンダリングすると、不一致が起こりやすくなります。
```react
// 問題のあるコード
function CurrentTime() {
  // サーバーとクライアントで時間が異なるため、不一致が発生します
  const [time, setTime] = useState(new Date().toLocaleTimeString());

  useEffect(() => {
    const timer = setInterval(() => {
      setTime(new Date().toLocaleTimeString());
    }, 1000);
    return () => clearInterval(timer);
  }, []);

  return <div>Current time: {time}</div>;
}
```

**3. 条件付きレンダリングの不一致**
このコードでは、`localStorage`はクライアントサイドでしか利用できないため、サーバーサイドとクライアントサイドでレンダリング結果が異なる可能性があります。
```react
// 問題のあるコード
import { useEffect, useState } from 'react';

export default function Welcome() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  useEffect(() => {
    setIsLoggedIn(localStorage.getItem('isLoggedIn') === 'true');
  }, []);

  return (
    <div>
      {isLoggedIn ? <h1>ようこそ、ユーザーさん！</h1> : <h1>ログインしてください</h1>}
    </div>
  );
}
```

## 解決策
上記のコード例は、それぞれの原因がどのように`Hydration Error`を引き起こす可能性があるかを示しています。
これらの問題を解決するためには、以下のようなアプローチを取ることができます。

1. クライアントサイド専用APIの使用は`useEffect`内に限定する。
2. 動的なデータの初期値をサーバーサイドとクライアントサイドで一致させる。
3. 条件付きレンダリングの初期状態を明示的に設定し、クライアントサイドでのみ更新する。

以下がその修正コード例になります。
**1. クライアントサイド専用APIの使用は`useEffect`内に限定する。**
```react
// 改善されたコード
function WindowSize() {
  const [width, setWidth] = useState(0);

  useEffect(() => {
    setWidth(window.innerWidth);
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  if (width === 0) return <div>Loading...</div>;
  return <div>Window width: {width}px</div>;
}
```

**2. 動的なデータの初期値をサーバーサイドとクライアントサイドで一致させる。**
```react
// 改善されたコード
function CurrentTime() {
  const [time, setTime] = useState('');

  useEffect(() => {
    // useEffect内でクライアントサイドでのみ時間を設定
    setTime(new Date().toLocaleTimeString());
    const timer = setInterval(() => {
      setTime(new Date().toLocaleTimeString());
    }, 1000);
    return () => clearInterval(timer);
  }, []);

  if (!time) return <div>Loading...</div>;
  return <div>Current time: {time}</div>;
}
```

**3. 条件付きレンダリングの初期状態を明示的に設定し、クライアントサイドでのみ更新する。**
```react
// 改善されたコード
import { useEffect, useState } from 'react';

export default function Welcome() {
  const [isLoggedIn, setIsLoggedIn] = useState(null);

  useEffect(() => {
    setIsLoggedIn(localStorage.getItem('isLoggedIn') === 'true');
  }, []);

  if (isLoggedIn === null) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      {isLoggedIn ? <h1>ようこそ、ユーザーさん！</h1> : <h1>ログインしてください</h1>}
    </div>
  );
}
```

## まとめ
Hydration Errorは一見厄介に見えますが、原因を理解し適切に対処すれば、簡単に解決できる問題です。主なポイントは以下の通りです

**1. クライアントサイド限定のAPIはuseEffect内で使用する
2. 初期状態をサーバーサイドとクライアントサイドで一致させる
3. 必要に応じて、データ取得中は代替のコンテンツを表示する**


これらのポイントを押さえておけば、Next.jsでのHydration Errorとの戦いも、きっと楽になるはずです。