ハイドレーションエラー徹底ガイド

～分かりやすい具体例で学ぶ、サーバーとクライアントのズレを防ぐ方法～

Next.js などのサーバーサイドレンダリング（SSR）対応フレームワークでは、サーバーで生成された HTML とクライアント側で React が再レンダリングする際に、両者が一致しないと「ハイドレーションエラー」が発生します。
ここでは、なぜハイドレーションエラーが起こるのか、そして具体的なコード例を多数交えながら、エラーを防ぐための方法を解説します。

## 1. ハイドレーションエラーとは？

### ハイドレーションの流れ
1.	サーバーサイドレンダリング（SSR）
•	サーバーが HTML を生成してクライアントに送信する
•	例：初回アクセス時に 
```
<div>カウント: 0</div>
```
が返される
  
2.	クライアント側でハイドレーション
•	React がサーバー生成の HTML に対して、イベントハンドラの取り付けや内部状態の管理を行い、インタラクティブにする
•	このとき、サーバーとクライアントで出力が一致しないとエラーとなる

### エラー発生の典型例
•	状態が違う
サーバーでは「0」でレンダリングされたのに、クライアント側ですぐに「1」に更新される場合
•	ランダムな値や日時の生成
毎回変わる値（乱数や現在日時）を直接レンダリングすると、サーバーとクライアントで異なる内容になる
•	ブラウザ固有APIの利用
window や document などサーバーでは存在しない値を直接使うと、結果が一致しません

## 2. 具体例で学ぶエラー防止策

### 2.1 状態の一致を保つ

問題の例：不一致の状態更新
```typescript
export default function CounterError() {
  // サーバーでは初期値 0 でレンダリングされるが、
  // クライアント側で useEffect により即座に更新してしまう場合…
  const [count, setCount] = useState(0);

  // クライアントだけで状態を更新する処理（ここでは即時実行）
  useEffect(() => {
    // 何らかの理由で初回レンダリング直後に更新
    setCount(1);
  }, []);

  return (
    <div>
      <p>カウント: {count}</p>
    </div>
  );
}
```
問題点：
•	サーバーは「カウント: 0」とレンダリングするのに対し、クライアントは「カウント: 1」に変わるため、ハイドレーション時に「違いがある！」とエラーが出る

対策：初期状態を統一する
```typescript
export default function CounterFixed() {
  // 初期値は両方で 0 に統一
  const [count, setCount] = useState(0);

  // クライアントでの更新はユーザー操作後に実施
  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}
```
ポイント：
•	初回レンダリング時に状態がサーバーとクライアントで同じ値（この例では 0 ）であることが重要

### 2.2 ブラウザ依存の処理は useEffect 内に隔離する

問題の例：ブラウザAPIの直接利用
```typescript
export default function TimeNowError() {
  // サーバー側では window が存在しないためエラーになる
  const now = new Date().toLocaleTimeString();

  return <div>現在時刻: {now}</div>;
}
```
問題点：
•	サーバー側で new Date().toLocaleTimeString() を実行すると、クライアント側で再評価した時刻とズレが生じる可能性がある

対策：useEffect でクライアント専用の処理にする
```typescript
import { useEffect, useState } from "react";

export default function TimeNowFixed() {
  const [now, setNow] = useState("読み込み中...");

  useEffect(() => {
    // この処理はクライアント側でのみ実行されるので安全
    setNow(new Date().toLocaleTimeString());
  }, []);

  return <div>現在時刻: {now}</div>;
}
```
ポイント：
	•	サーバー側では初期状態（“読み込み中…”）でレンダリングし、クライアント側でのみ更新する

### 2.3 クライアント専用コンポーネントを動的インポートする

問題の例：ブラウザ依存の重い処理

たとえば、地図の表示や WebGL を使ったアニメーションなど、ブラウザ固有のライブラリを使う場合、サーバー側でレンダリングするとエラーやパフォーマンスの問題が起こる

対策：Next.js の dynamic インポートで ssr を無効化する
```typescript
import dynamic from "next/dynamic";

// ssr: false に設定してサーバー側ではレンダリングしない
const MapComponent = dynamic(() => import("../components/MapComponent"), {
  ssr: false,
});

export default function Page() {
  return (
    <div>
      <h1>地図の表示（クライアント専用）</h1>
      <MapComponent />
    </div>
  );
}
```
ポイント：
•	MapComponent はクライアント側でのみ読み込まれるため、サーバーとクライアントのレンダリング差をなくす

2.4 条件付きレンダリングでタイミングを調整する

問題の例：コンポーネントがマウントされる前にブラウザ依存の処理を実行する

直接レンダリングすると、サーバー側とクライアント側の結果が合わずエラーになる可能性がある

対策：マウント後にレンダリングする仕組みを作る
```
import { useEffect, useState } from "react";

export default function ClientOnlyContent() {
  const [isMounted, setIsMounted] = useState(false);

  useEffect(() => {
    // コンポーネントがクライアント側でマウントされたら状態を更新
    setIsMounted(true);
  }, []);

  // マウント前は何もレンダリングしない
  if (!isMounted) {
    return null;
  }

  return <div>このコンテンツはクライアント側でのみ表示されます</div>;
}
```
ポイント：
•	初回レンダリング時に何も表示しないことで、サーバー側とクライアント側の出力が一致する

## 3. まとめ

ハイドレーションエラーは、サーバー側で生成した HTML とクライアント側で再レンダリングした内容が一致しないときに発生します。
このエラーを防ぐためには、次のポイントが重要です：
•	状態の初期値を統一する
•	サーバーとクライアントで同じ状態でレンダリングを開始すること
•	ブラウザ固有の処理は useEffect に隔離する
•	クライアント側でのみ実行される処理に限定する
•	動的インポートでクライアント専用コンポーネントを分離する
•	ssr: false を利用してサーバー側のレンダリングから除外する
•	条件付きレンダリングでタイミングを調整する
•	マウント後に表示することで不一致を回避する

具体例を参考に、各シナリオでどのように対策すればよいかを確認しながら実装してみましょう。これらのテクニックを使えば、ハイドレーションエラーの発生を防ぎ、ユーザーにスムーズな体験を提供できるはずです。