代表的な面接トピック

フロントエンド面接:ReactのHydration Mismatch(ハイドレーション不整合)をデバッグおよび修正する方法

フロントエンド難しい
Offer.cc 編集チーム公開日 更新日

質問

Next.js App Routerのページにおいて、本番環境の一部のユーザーでReactエラー #418が報告されています。時刻、テーマ、またはフォームIDがサーバーHTMLと異なることがあり、ボタンが再生成される場合もありますが、ローカル開発環境では安定して再現しません。ハイドレーションの規約(コントラクト)を説明し、診断手順を設計し、非決定論的レンダリング、ブラウザ状態、不正なHTML、外部ミューテーションを修正した上で、useEffect、ssr: false、またはsuppressHydrationWarningがどのような場合に正当化されるかを述べてください。

問題と適用されるコンテキスト

Next.js App Routerの製品ページはHTMLを事前レンダリング(prerender)し、その後ブラウザでJavaScriptを読み込んで、お気に入りボタンと再入荷通知フォームをインタラクティブにします。本番モニタリングでは、初回アクセスのごく一部でReactエラー #418が報告されています。影響を受けたユーザーには、時刻やテーマがチラつく、フォームラベルの関連付けが一瞬外れる、またはお気に入りボタンが再生成されるといった現象が発生する可能性があります。ローカル開発環境やその後のクライアントサイド遷移では、通常正常に表示されます。

コードレビューの結果、同じClient Componentのレンダリングパス内に4つのリスクのある処理が見つかりました。ブラウザのデフォルトタイムゾーンを使用した時刻のフォーマット、localStorageからのテーマによる分岐、Math.random()によるフォームIDの生成、そして不正なネストを引き起こす可能性のあるリッチテキストの出力です。本番環境の前段にはCDNが存在し、一部のユーザーはページを変更するブラウザ拡張機能を使用しています。ページ全体でSSRを無効化したり、警告を広範囲に抑制したりすることなく、サーバーのスナップショットがクライアントの初回レンダリングと異なる原因となっているステージを特定する方法を説明してください。

KORE1の2026年シニアフロントエンド面接ガイドでは、ローカルで再現できない本番環境固有のハイドレーション不整合のデバッグが直接問われます。MockIFの2026年React問題集でも、ハイドレーションは高度なアーキテクチャ問題として挙げられています。ReactおよびNext.jsのドキュメントでは同一出力の規約が定義されており、時刻、ランダムな値、ブラウザAPI、不正なネスト、外部DOMミューテーションが原因として特定されています。したがって、この質問は現在の面接において検証可能な代表性を持っています。そのコアスキルはブラウザのレンダリングとReactハイドレーションのデバッグであるため、カテゴリはfrontendです。イベントループ、キャッシュ、パフォーマンス、key、stale closureに関する既存の質問では、この規約はカバーされません。

面接官が評価しているポイント

第1に、候補者がハイドレーションを正確に定義できるか。初回ロード時、ユーザーには事前レンダリングされたHTMLが表示されます。ブラウザがそれをパースし、Reactは既存のDOMにコンポーネントロジックとイベントハンドラをアタッチします。Client Componentも初期HTMLの事前レンダリングに参加できます。"use client"は、初回ロード時にブラウザのみでレンダリングすることを意味するわけではありません。この規約は、サーバーの結果とクライアントの「初回レンダリング」を比較するものであり、その後の任意の2回のレンダリングを比較するものではありません。

第2に、候補者が証拠に基づいて4つの不整合の分類を区別できるか。データスナップショット、現在時刻、ランダムな値など、アプリケーションの入力が異なる場合があります。windowlocalStorage、言語、タイムゾーンなど、実行環境が異なる場合もあります。ブラウザが不正なHTMLを修復して異なるDOMにする場合もあります。最後に、CDN、拡張機能、またはスクリプトがReactの開始前にノードを変更(ミューテーション)する場合があります。コンポーネントスタックだけを見ていると、それより前に発生したパースや外部ミューテーションを見逃す可能性があります。

第3に、候補者が各原因を適切な修正方法に対応付けられるか。安定した入力はシリアライズして1つのスナップショットとして再利用する必要があります。ユーザー環境はサーバー上でCookieやプロファイルから解決するか、ハイドレーション後にEffect内で更新できます。ランダムなDOM IDにはuseIdを使用するべきです。不正なネストは修正する必要があります。SSRは、事前レンダリングが本当に不可能な小さなサードパーティ製コンポーネントに対してのみ無効化するべきです。

最後に、候補者が不具合の解消を証明できるか。優れた計画には、本番ビルド、コールド初回ロード、低速JavaScript、ロケールとタイムゾーンの組み合わせ、キャッシュヒット、クリーンなブラウザ、拡張機能の影響を受けたブラウザが含まれます。エラー、DOMの一致、インタラクション状態、視覚的なチラつきをチェックします。コンソールが静かなことは結果の1つに過ぎず、完全な証拠ではありません。

回答前の明確化のための質問

  • エラーはドキュメントの初回ロード時のみ発生しますか、それともクライアント遷移時にも発生しますか? ハイドレーションは、Reactが既存のHTMLを初めて引き継ぐ処理です。遷移後の不具合は、状態、キャッシュ、または非同期の競合状態(race condition)を示唆しています。
  • 不整合はテキスト、属性、ノード構造、それともサブツリー全体にありますか? 切り詰められた本番環境のエラー番号から推測するのではなく、完全なエラー、コンポーネントスタック、ルート、デプロイバージョンを保持してください。
  • サーバーはユーザーのロケール、タイムゾーン、テーマ、実験バケットを把握していますか? URL、Cookie、またはプロファイルを通じて利用可能な値はサーバー上で確定し、クライアントに渡す必要があります。不明な値には、安定したプレースホルダーまたはハイドレーション後の更新が必要です。
  • クライアントの初回レンダリングは、HTMLを生成したのとまったく同じビジネスデータスナップショットを使用していますか? ブラウザがハイドレーション前により新しいデータを取得したとしても、初期ビューはシリアライズされたスナップショットから開始し、ハイドレーション後にのみ更新されるべきです。
  • HTMLはCDN、翻訳プロキシ、セキュリティインジェクター、またはオプティマイザを通過していますか? オリジンとエッジの両方のレスポンスをキャプチャして、仲介機能が空白、属性、タグ、またはスクリプトの順序を変更していないか確認します。
  • エラーは特定の拡張機能、モバイルブラウザ、または地域に限定されていますか? これにより、Reactが開始する直前の実際のDOMとネットワークレスポンスボディを比較すべきかどうかが決まります。
  • このサブツリーは事前レンダリングが必要ですか? SEO、ファーストペイント、またはJavaScriptなしでの可読性に価値があるコンテンツはSSRを維持すべきです。ブラウザ専用の末端ウィジェットであれば、局所的なssr: falseが正当化される場合があります。
  • どのような視覚的変化が許容されますか? 2段階レンダリング(Two-pass rendering)はハイドレーション規約を維持できますが、低速回線では目に見える変化を引き起こす可能性があります。プレースホルダーの内容、レイアウトの安定性、アクセシビリティについて事前に合意してください。

30秒の回答フレームワーク

「ハイドレーションでは、サーバーHTMLとクライアントの初回レンダリングが同一の構造とコンテンツを生成することが求められます。私ならエラー、コンポーネントスタック、ルート、バージョン、ロケール、タイムゾーンを記録し、3つの証拠を照合します。すなわち、ネットワーク経由で受信したHTML、Reactが引き継ぐ前にブラウザがパースしたDOM、そしてクライアントの初回レンダリングへの入力です。ネットワークHTMLがすでに異なっている場合はデータスナップショットとCDNを調査します。パース中に変化した場合は不正なネスト、拡張機能、初期スクリプトを調査します。Reactのレンダリング時に分岐する場合は時刻、ランダム性、ブラウザAPI、早期のデータ更新を調査します。シリアライズされた1つのスナップショットを再利用し、localeとtimeZoneを指定し、useIdを使用し、ブラウザ状態はCookieから導出するかハイドレーション後にEffect内で更新します。ssr: falseはブラウザ専用の末端コンポーネントに限定し、suppressHydrationWarningは回避不能な1階層のテキスト差分にのみ使用します。その後、環境マトリクス全体で本番のコールドロードを実行し、回復可能エラー、サブツリーの再生成、チラつき、インタラクションの不具合がないことを検証します」

ステップごとの詳細解説

ステップ1:コンポーネントをデバッグする前に不変条件(invariant)を定義する。

サーバーコンポーネントツリーが入力スナップショットSからHTMLを生成するとします。ブラウザはそのHTMLをパースしてDOM Dにし、クライアントは入力Cで初回レンダリングを実行します。ハイドレーションを成功させるには、Dとクライアントツリーが、Reactが依存するノード、テキスト、属性に関して一致する必要があります。目標は、単に両方の環境が同じソースファイルを読み込んだことを証明することではありません。S、パース、およびCが同じ結果を生成したことを証明することです。

Reactはすべての属性の差異がパッチされることを保証していません。一部の不整合は回復可能(recoverable)でありサブツリーが再生成されますが、最悪の場合、ハンドラが間違った要素にアタッチされる可能性があります。したがって、ハイドレーション不整合は無害なコンソールノイズではありません。

ステップ2:本番環境の初回ロードを代表する証拠パッケージを構築する。

機密データを収集することなく、ルート、デプロイバージョン、リクエストID、キャッシュステータス、ロケール、タイムゾーン、テーマのソース、実験バケット、User-Agent、完全なコンポーネントスタックを記録します。本番ビルドを使用し、完全なリロードを行います。ホットリロード、開発環境固有の動作、クライアント遷移では、本番のコールドスタートを再現できません。

1つのリクエストに対して、3つのアーティファクトを保持します。オリジンまたはCDNから返されたレスポンスボディ、JavaScriptを無効にしてパースされたDOM、およびJavaScriptを有効にしてエラーの直前と直後に取得されたDOMです。レスポンスは正しいもののElementsツリーがすでに異なっている場合は、まずHTMLパーサーの修復、拡張機能、React前のスクリプトを調査します。React開始時にのみDOMが分岐する場合は、初回レンダリングの入力を比較します。

ステップ3:レンダリングパスから非決定論的要素(nondeterminism)を排除する。

次のコードは、サーバーとブラウザで異なる環境の使用を許容し、レンダリングごとに新しいIDを生成してしまいます。

tsx
"use client"

export function StockNotice({ updatedAt }: { updatedAt: string }) {
  const isBrowser = typeof window !== "undefined"
  const theme = isBrowser ? localStorage.getItem("theme") ?? "light" : "light"
  const inputId = `stock-${Math.random()}`

  return (
    <section data-theme={theme}>
      <time dateTime={updatedAt}>{new Date(updatedAt).toLocaleString()}</time>
      <label htmlFor={inputId}>Back-in-stock alert</label>
      <input id={inputId} />
    </section>
  )
}

安定した値はサーバー上で解決し、それらの正確な値をClient Componentの初期入力として渡すアプローチを優先してください。ロケール、タイムゾーン、テーマがURL、Cookie、またはプロファイルで利用可能な場合は、サーバー上で読み取ります。日付フォーマットには明示的なlocaleとtimeZoneを指定します。サーバーとクライアントのコンポーネントツリー自体が同一である限り、DOM IDにはuseIdを使用するべきです。

tsx
"use client"

import { useId } from "react"

interface StockNoticeProps {
  updatedAt: string
  updatedLabel: string
  initialTheme: "light" | "dark"
}

export function StockNotice({
  updatedAt,
  updatedLabel,
  initialTheme,
}: StockNoticeProps) {
  const inputId = useId()

  return (
    <section data-theme={initialTheme}>
      <time dateTime={updatedAt}>{updatedLabel}</time>
      <label htmlFor={inputId}>Back-in-stock alert</label>
      <input id={inputId} />
    </section>
  )
}

サーバーがブラウザのタイムゾーンを本当に把握できない場合は、両側で同じUTC表記または固定プレースホルダーをレンダリングし、ハイドレーション後にuseEffect内で置き換えます。これによりレンダリングが1回増えるため、表示領域を確保し、低速回線での視覚的な変化を評価してください。ビジネスデータにも同じルールを適用します。HTMLを生成したスナップショットをシリアライズし、それを初回クライアントレンダリングに使用し、バックグラウンド更新によるビューの更新はハイドレーション後にのみ行います。

ステップ4:ブラウザのパースと外部ミューテーションを調査する。

ブラウザは不正なHTMLを寛容に修復します。段落(pタグ)内のブロック要素や、ボタン内のボタンなどは、Reactが出力した構造とは異なるパース済みDOMを生成する可能性があります。セマンティクスとネストを修正し、HTMLバリデーションとDOMインスペクションで検証します。Effectは不正な構造に対する解決策ではありません。

エッジキャッシュヒット時のみ障害が発生する場合は、オリジンとエッジのレスポンスを比較し、HTMLを書き換えている変換処理を無効化します。特定の拡張機能でのみ発生する場合は、クリーンなプロファイルで再現し、Reactの前に拡張機能が何を変更したかを特定します。アプリケーションはその影響を計測することはできますが、制御不能な拡張機能が存在するからといって、アプリケーション自体の本質的な不整合をすべて隠蔽するべきではありません。サードパーティ製スクリプトも同様に二分探索(bisect)します。初期パスから1つずつ除外し、最初のDOM書き込みを特定します。

ステップ5:コストに基づいてエスケープハッチを選択する。

useEffectはハイドレーション後にしか読み取れない環境データに適していますが、2回目のレンダリングが発生します。dynamicssr: falseの組み合わせは、サーバー上で実行できず、重要なSEOや初期コンテンツを持たないローカルなサードパーティ製コンポーネントに適しています。ページ全体をクライアント専用レンダリングに変換すると、事前レンダリングの価値が失われ、空白期間が長くなります。suppressHydrationWarningは浅い(shallowな)エスケープハッチとしてのみ機能し、Reactは抑制されたテキストをパッチしません。これは回避不能で孤立した差異のためのものであり、タイムゾーン、ランダムID、テーマ、または古いデータに対する修正手段ではありません。

ステップ6:障害マトリクスを用いて修正を証明する。

少なくとも2つのロケール、2つのタイムゾーン、ライト/ダークテーマ、ログイン/未ログイン状態、キャッシュのヒット/ミス、通常/遅延JavaScript、クリーンなプロファイル、既知の拡張機能をカバーします。ケースごとに完全なリロードを実行します。回復可能なハイドレーションエラーが発生しないこと、重要なサブツリーが再生成されないこと、フォーカスやイベントの挙動が正しいままであること、意図したタイミングで時刻やテーマが更新されること、レイアウトの視覚的なズレがないことを確認します。

Date.nowMath.random、暗黙のtoLocaleString、レンダリング中のwindowlocalStorage、危険なネスト、広範囲なsuppressHydrationWarningに対するコードレビュー手順を追加します。これにより、一度解決したインシデントが同じ種類のミスによって再発するのを防ぎます。

高品質な回答例

「私ならまず、このインシデントの対象をドキュメントの初回ロードに限定します。Next.jsは初期リクエスト時にClient Componentを事前レンダリングできます。ブラウザがHTMLを受信した後、Reactはハンドラをアタッチする前に、同一の初期入力から同一のツリーを生成しなければなりません。対象のコンポーネントはlocalStorageを読み取り、ブラウザのデフォルトタイムゾーンを使用し、レンダリング中にランダムなIDを生成しているため、これら3つの値すべてがサーバーと異なる可能性があります。また、リッチテキストの構造が、Reactが処理する前にブラウザによって修復されている可能性もあります。

実際の事象を1つ取り上げ、その完全なコンポーネントスタック、デプロイ、locale、timeZone、テーマのソース、キャッシュステータスをキャプチャします。ネットワークレスポンスを保存し、React開始前のDOMと比較します。レスポンスがすでに誤っている場合はサーバーのスナップショットとCDNを調査します。パースによって変化している場合は不正なネスト、拡張機能、初期スクリプトを調査します。React開始時にのみ変化する場合は、初回クライアントpropsと環境分岐を比較します。

修正としては、サーバー側でCookieまたはプロファイルからlocale、timeZone、初期テーマを解決し、updatedLabelをフォーマットして、ISO時間とともに渡します。初回クライアントレンダリングではそれらの値をそのまま使用します。ランダムIDはuseIdに置き換え、HTMLのセマンティクスを修正します。サーバーが認識できないブラウザ情報は安定したプレースホルダーから開始し、Effect内で更新します。ビジネスデータはシリアライズされたサーバースナップショットから開始し、ハイドレーション後にバックグラウンドで更新します。

SSRをグローバルに無効化したり、修正可能な警告を抑制したりはしません。局所的なssr: falseはブラウザ専用のサードパーティ末端コンポーネントにのみ使用し、suppressは回避不能な1階層の表示差分にのみ適用します。最後に、ロケール、タイムゾーン、テーマ、キャッシュ、低速回線、クリーンなブラウザの組み合わせで本番コールドロードをテストし、エラー #418、サブツリーの再生成、フォーカス喪失、視覚的なチラつきがないことを検証します」

よくある間違い

  • "use client"はサーバーHTMLが存在しないことを意味すると誤認する。 Client Componentも初回ロード時には事前レンダリングされます。
  • JSX内でtypeof windowを使用して2つの異なるUIをレンダリングする。 サーバーの分岐とブラウザの初回レンダリングは必然的に異なってしまいます。
  • すべての値をEffectに移動して解決した気になってしまう。 これにより不整合を先送りするだけで、2回目のレンダリング、空白コンテンツ、レイアウトシフト(CLS)が発生し、低速回線での体験が悪化します。
  • 広範なルート要素にsuppressHydrationWarningを追加する。 これは浅い階層にしか効かず、Reactが不一致テキストを自動修復してくれるわけでもありません。
  • 本番エラーを見てページ全体をssr: falseに変更する。 根本原因を説明することなく、事前レンダリングの利点を捨て去ることになります。
  • ページのソース表示とハイドレーション後のDOMだけを比較する。 これではReactが開始する前のブラウザパース済みDOMが見落とされます。
  • ローカル開発環境とクライアント遷移のみでテストする。 本番ビルド、完全リロード、エッジキャッシュ、タイムゾーン、または拡張機能が、元の障害を再現するために必要となる場合があります。
  • コンソールエラーが消えた時点で調査を終える。 サブツリーの再生成、誤ったハンドラ、フォーカスの喪失、視覚的なチラつき、誤ったビジネスデータがないことも確認する必要があります。

フォローアップ質問と回答

なぜすべてのタイムスタンプにsuppressHydrationWarningを追加してはいけないのですか?

これは1階層のみのエスケープハッチであり、Reactが抑制されたテキストを代わりに修復してくれるわけではありません。2つの環境で異なるデフォルトタイムゾーンが使用されているためにタイムスタンプが異なる場合、明示的なlocaleとtimeZoneを指定するか、共通のフォーマット済み値を使用することが根本的な修正になります。抑制(suppression)は、製品仕様としてハイドレーション後に意図的に異なる値を受け入れ、その差異をどうしても排除できない場合にのみ使用してください。視覚的な動作と支援技術(スクリーンリーダー等)が読み上げる内容の両方を検証する必要があります。

useEffectとssr: falseはどのように使い分けますか?

コンポーネントが安定したシェルと価値ある事前レンダリングコンテンツを持ち、1つのラベルのみがブラウザ情報に依存している場合は、SSRを維持し、ハイドレーション後にその小さな部分を更新します。末端コンポーネント全体がサーバー上で実行できないサードパーティライブラリに依存しており、有用な事前レンダリング出力がなく、サイズが安定したローディング状態を提供できる場合は、局所的にSSRを無効化します。警告を最も早く消せる方法ではなく、コンテンツの価値、依存関係の境界、視覚的コストに基づいて判断します。

両方の環境で同じReactコードを実行しているのに、なぜ不正なHTMLがハイドレーションを破壊するのですか?

サーバーは文字列を送信し、ブラウザはHTMLパースルールに従ってDOMを構築および修復します。不正なネストは自動的に閉じられたり、移動されたり、削除されたりする可能性があります。そのため、Reactは元の文字列が記述しているように見えるツリーではなく、修復されたツリーを引き継ぐことになります。レスポンスボディとReact前のDOMを比較し、セマンティクスを修正してHTMLを検証してください。

本番環境固有の障害が断続的(間欠的)な場合、どのようにオブザーバビリティ(可観測性)を追加しますか?

サンプリングとプライバシーフィルタリングを適用した上で、完全な回復可能エラーまたはコード、コンポーネントスタック、ルート、デプロイバージョン、locale、timeZone、テーマのソース、キャッシュステータス、ブラウザタイプを収集します。重要な初回レンダリング入力に対して不可逆なダイジェスト(ハッシュ)またはバージョンを記録し、実際の値をログ出力せずにサーバーとクライアントのスナップショットを比較できるようにします。メトリクスをルート、バージョン、環境ごとにスライスして分析します。修正後、エラー率がゼロになったことだけでなく、サブツリーの再生成、インタラクション、視覚的メトリクスが悪化(デグレード)していないことを確認します。

公開情報ソース

関連する質問