React Client Component の二重実行とハイドレーションエラー対策
React Server Components (RSC) を使用する際、多くの開発者が「Client Component はクライアントでのみ実行される」と誤解しがちです。
しかし実際には、Client Component はサーバーとクライアントの両方で実行されるという重要な特性があります。
この二重実行により、サーバーとクライアントで異なる値が生成される場合(時刻、ランダム値、ブラウザAPIなど)、ハイドレーションエラーが発生します。
この記事では、この問題の原因と具体的な解決方法を解説します。
結論
Client Component はサーバ側とクライアント側で2回実行される
Client Component = クライアントでのみ実行されるコンポーネントではない
検証
こんなClient Componentを作ってみる
'use client';
export default function ClientComponent() {
console.log('実行されました!', new Date().toISOString());
return <p>現在時刻: {new Date().toISOString()}</p>;
}
実行タイミング
このコンポーネントが実行されるタイミングは以下となる。
- サーバー実行 (SSR)
- 初期HTML生成
- クライアント実行 (ハイドレーション)
- インタラクティブ機能追加
コンソール出力
[サーバー] 実行されました! 2025-06-07T10:30:45.123Z
[ブラウザ] 実行されました! 2025-06-07T10:30:45.891Z
二重実行の詳細プロセス
1. サーバーでの実行 (Pre-rendering)
// Node.js環境で実行
'use client';
export default function TimeComponent() {
// サーバーの時刻で実行される
const time = new Date().toISOString(); // "2025-06-07T10:30:45.123Z"
return <p>{time}</p>;
}
生成されるHTML
<p>2025-06-07T10:30:45.123Z</p>
2. クライアントでの実行 (Hydration)
// ブラウザ環境で実行(同じコード)
'use client';
export default function TimeComponent() {
// ブラウザの時刻で実行される
const time = new Date().toISOString(); // "2025-06-07T10:30:45.891Z"
return <p>{time}</p>;
}
生成されるHTML
<p>2025-06-07T10:30:45.891Z</p> <!-- サーバーと違う! -->
そしてハイドレーションエラーの発生😅
Warning: Text content did not match.
Server: "2025-06-07T10:30:45.123Z"
Client: "2025-06-07T10:30:45.891Z"
Error: Hydration failed because the initial UI does not match
what was rendered on the server.
なぜ二重実行が必要なのか?🤔
SSR (Server-Side Rendering) の利点
// Server Component
export default function Page() {
return (
<div>
<h1>ブログ記事</h1>
<ClientInteractiveButton /> {/* これも初期HTMLに含まれる */}
</div>
);
}
// Client Component
'use client';
export default function ClientInteractiveButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'} いいね
</button>
);
}
ユーザー体験の流れ
- 0ms: サーバーからHTML受信
<div>
<h1>ブログ記事</h1>
<button>🤍 いいね</button> <!-- 見えるけどクリックできない -->
</div>
- 1000ms: JavaScriptダウンロード完了
- 1100ms: ハイドレーション完了
<button onclick="...">🤍 いいね</button> <!-- クリック可能になった! -->
use client
の真の意味
'use client'; // これは「指示」であり「制限」ではない
実際の意味:
✅ "このコンポーネントをクライアントバンドルに含めて"
✅ "ブラウザでインタラクティブにして"
✅ "でも、SSRのためにサーバーでも実行して"
❌ "クライアントでのみ実行して" ← これは間違い
問題が発生するパターン
1. 時間依存の値
// ❌ 問題のあるコード
'use client';
export default function CurrentTime() {
return <p>現在時刻: {new Date().toISOString()}</p>;
}
2. ランダム値
// ❌ 問題のあるコード
'use client';
export default function RandomNumber() {
return <p>ランダム: {Math.random()}</p>;
}
3. ブラウザAPI依存
// ❌ 問題のあるコード
'use client';
export default function WindowSize() {
return <p>画面幅: {window.innerWidth}px</p>; // サーバーでwindowは存在しない
}
4. ユーザー固有データ
// ❌ 問題のあるコード
'use client';
export default function UserPreference() {
const theme = localStorage.getItem('theme'); // サーバーでlocalStorageは存在しない
return <p>テーマ: {theme}</p>;
}
解決方法
1. useEffect を使用する方法
useEffectを使用することで、遅延実行を行います。ハイドレーション実行時のtimeの値はnullとなり、生成されるHTMLが一致します。これにより、ハイドレーションエラーが発生しません。
'use client';
import { useState, useEffect } from 'react';
export default function SafeCurrentTime() {
const [time, setTime] = useState<string | null>(null);
useEffect(() => {
// この部分は本当にクライアントでのみ実行される
setTime(new Date().toISOString());
}, []);
// サーバー実行時とハイドレーション時は null
if (time === null) {
return <div>時刻読み込み中...</div>; // プレースホルダー
}
// クライアントでの状態更新後のみ実際の時刻表示
return <p>現在時刻: {time}</p>;
}
実行フロー
- サーバー: time = null → "時刻読み込み中..." 表示
- 初回ハイドレーション: time = null → "時刻読み込み中..." 表示(一致✅)
- useEffect実行: setTime() → 再レンダリング → 実際の時刻表示
2. useSyncExternalStoreを使用する方法
useSyncExternalStore
の第3引数getServerSnapshot
は、サーバーレンダリング時とハイドレーション時にのみ呼び出される特性を利用した方法です。この仕組みを活用することで、サーバーとクライアントで異なる値を返しながらも、ハイドレーションエラーを回避できます。
本来このフックは外部ストアとの同期のために設計されましたが、環境判定においてはこの特殊な実行タイミングを「ハック的」に利用します。
'use client';
import { useSyncExternalStore } from 'react';
export function useIsClient() {
return useSyncExternalStore(
() => () => {}, // subscribe: 空の関数
() => true, // getSnapshot: クライアント実行時
() => false // getServerSnapshot: サーバー実行時
);
}
'use client';
export default function SmartCurrentTime() {
const isClient = useIsClient();
if (!isClient) {
return <div>--:--:--</div>; // サーバー用プレースホルダー
}
return <p>現在時刻: {new Date().toISOString()}</p>; // クライアントでのみ実行
}
- サーバー: getServerSnapshot() → false → "--:--:--" 表示
- 初回ハイドレーション: getServerSnapshot() → false → "--:--:--" 表示(一致✅)
- ハイドレーション完了: getSnapshot() → true → 実際の時刻表示
まとめ
Client Component は「クライアント専用」ではなく、「サーバーとクライアントの両方で実行される特別なコンポーネント」です。
この二重実行の性質を理解し、適切に対処することで、
- ハイドレーションエラーを防げる
- SSRの恩恵を受けながらインタラクティブ機能を提供できる
- ユーザー体験を最適化できる
ということができます。
Discussion