Claude Media
Claude Codeでhydrationエラーを追う — Text content does not matchの原因

Claude Codeでhydrationエラーを追う — Text content does not matchの原因

Text content does not match server-rendered HTMLが出たとき、Claude CodeにChromeのコンソールを読ませ、Date・window・入れ子のタグなど原因を絞り込む手順です。

hydrationエラーは、サーバーが返したHTMLと、ブラウザの最初の描画結果が食い違ったときに出ます。Next.jsでは「Text content does not match server-rendered HTML」という文言で表示される典型例です。原因はコードの中にあることが多い一方、ブラウザ拡張やCDNのように、コードの外にある場合もあります。

Claude CodeにChromeを繋ぐと、コンソールに出た警告を読ませたうえで、疑わしいコードを調べさせる流れが作れます。ここでは、原因の候補を先に表にして、どの順で絞るかと、直し方の選び方をまとめます。

まず前提を3つ確かめる

Chrome連携は、どの環境でも使えるわけではありません。条件に合わないと、コンソールを読ませる前の段階で止まります。

  • Claude in Chrome拡張機能のバージョン1.0.36以降と、Pro・Max・Team・Enterpriseのいずれかのプランが要ります
  • /loginでサインインしていることが必要です。APIキーやclaude setup-tokenで認証していると、--chromeを付けてもChrome連携はオフのままになります
  • Amazon BedrockなどのサードパーティプロバイダーだけでClaudeを使う環境では、Chrome連携は使えません。WSLも対象外です

起動はclaude --chromeで、接続状態は/chromeで確かめます。毎回フラグを付けたくない場合は、/chromeで「Enabled by default」を選べます。ただしブラウザツールが常時ロードされ、コンテキストの使用量が増えます。接続そのもので詰まるときは、Claude CodeがChromeに接続できないときの原因と対処に症状別の切り分けがあります。

許可していないサイトを操作するときは、Claude in Chrome wants toで始まる確認が出ます。サイト単位の許可はChrome拡張機能の設定が持っています。プランモードでは、新しいタブを開く操作にも確認が入ります。

コンソールを読ませて、まず再現させる

最初にやるのは、エラーが本当に出るページと操作を固定することです。Chrome連携のドキュメントは、ログが長くなりやすいので「全部出して」ではなく、探すパターンを伝えるよう勧めています。

localhost:3000/dashboard を開いて、ページ読み込み直後のコンソールを確認してください。
「Hydration failed」「did not match」「Text content does not match」を含む
メッセージだけ拾って、全文と、どの要素に関する警告かを教えてください。
まだ何も修正しないでください。

「まだ修正しない」と添えるのがコツです。警告文を見た時点でClaudeが原因を決め打ちして書き換え始めると、本当の原因が別にあったとき、変更が増えて戻しにくくなります。

本番ビルドでは事情が変わります。文言が短縮され、418のような番号だけが出ます。Reactのエラーページは、この番号から元のメッセージを復元する作りです。コンソールに番号だけが出ている場合は、開発サーバーで再現させるほうが、警告の情報量は多くなります。

ローカルで再現させる手順そのものは、Claude CodeでChromeのコンソールログをデバッグするにも載っています。

原因は6系統に分けて考える

警告が取れたら、疑う順番を決めます。Next.jsとReactのドキュメントに挙がっている原因を、コンソールで見分けやすさ別に並べ直すと次のとおりです。

系統具体例見分け方
描画のたびに値が変わる具体例Date.now()、Math.random()、new Date()見分け方表示がちらつく文字列や時刻が警告の対象
ブラウザ専用APIを描画で使う具体例window、localStorage、typeof window !== 'undefined' の分岐見分け方サーバーとクライアントで分岐が割れる
地域設定による書式差具体例ユーザーのロケールで整形した日付見分け方サーバーの出力と自分のブラウザで表記が違う
HTMLの入れ子が不正具体例<p>の中に<div>、<a>の中に<a>見分け方警告が要素名に触れている
環境が書き換える具体例ブラウザ拡張、CDNの圧縮機能、iOSの電話番号リンク化見分け方拡張を切る、別端末で見ると変わる
構成の誤り具体例設定を誤ったCSS-in-JS見分け方スタイル周りのコンポーネントに警告が集中

上の4行(描画のたびに値が変わる、ブラウザ専用API、書式差、入れ子)は、コードを読めば原因が特定できます。環境が書き換える系統は、コードを直しても消えません。CSS-in-JSの誤りはコードではなく設定を見直す問題です。この区別が、後の手順を分けます。

Reactのエラーページは、ほかに「外部の変化するデータを、スナップショットなしでHTMLに載せた」場合も挙げています。ストアやフィードの値を描画に直接使っている場合が当たります。

切り分け

警告を見たら、この順に確かめる

  1. Q警告はpやdivなどのタグ名に触れていないか?
    答えが触れている

    タグの入れ子を疑います。<p>の中の<div>や<ul>、<a>の中の<a>などを探します。

    答えがはい
  2. Q警告の対象は、日時・乱数・ロケール整形の文字列とは無関係か?
    答えが関係している

    描画の中のDateやMath.random()が原因の可能性が高いです。値をクライアント側の描画に移します。

    答えがはい
  3. Q描画の中でwindowやlocalStorageを読まずに済んでいるか?
    答えが読んでいる

    typeof windowの分岐も含めて、初回描画の中から外します。

    答えがはい
  4. Q拡張機能を無効にした状態でも再現するか?
    答えが再現しない

    拡張機能がHTMLを書き換えています。コードの修正は不要で、環境側の問題として扱います。

    答えがはい

それでも残るとき

CDNの圧縮機能、CSS-in-JSの設定、iOSのリンク自動検出を順に確かめます。

コードの原因をClaudeに探させる

コードに起因する4系統は、grepで当たりをつけられます。Claudeに任せる前に、人が同じコマンドで確かめておくと、見落としを判断しやすくなります。

grep -rnE "Date\.now\(|new Date\(|Math\.random\(|localStorage|typeof window" \
  src --include="*.tsx" --include="*.jsx"

これは候補を洗い出す検索で、ヒットした箇所がすべて原因というわけではありません。イベントハンドラやuseEffectの中のDateは、初回描画に関与しないので問題になりません。描画の本体(コンポーネントの戻り値を組み立てる部分)にあるものだけが対象です。

Claudeには、警告で名前が出た要素から親をたどらせます。

先ほどの警告が指すのは「更新日時」の表示です。この文字列を出している
コンポーネントを特定し、描画中に値が変わる処理(Date・乱数・window・
localStorage・ロケール整形)がないか読んでください。
修正案は、直す箇所と理由を先に示してください。

直し方は3通りから選ぶ

Next.jsのドキュメントが挙げる対処は3つです。どれを選ぶかは、「サーバーのHTMLと最初の描画を揃えられるか」で決まります。

揃えられるなら、2回描画にする

最初はサーバーと同じ内容を描き、描画後に切り替えます。

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>
}

これはNext.jsのドキュメントにある例です。Reactのドキュメントは、この方法だと描画が2回走るためハイドレーションが遅くなると注意しています。遅い回線ではJavaScriptがHTMLよりかなり後に届き、直後にUIが切り替わって見えることもあります。

そのコンポーネントだけ、サーバーで描かない

ブラウザ専用のウィジェットなど、初回HTMLに出す意味が薄いものは、サーバー描画を切ります。

import dynamic from 'next/dynamic'
 
const NoSSR = dynamic(() => import('../components/no-ssr'), { ssr: false })

食い違いが避けられないなら、警告だけ止める

タイムスタンプのように、サーバーとクライアントで値が違って当然の要素にはsuppressHydrationWarning={true}を付けます。

<time dateTime="2016-10-25" suppressHydrationWarning />

ここには制約が2つあります。効くのはその要素の1階層だけで、深い子孫には届きません。もう1点、このフラグを付けても、Reactは食い違ったテキストを直しません。警告が消えるだけで、画面にはサーバーの値が残ります。何を直すのかを指定して頼みます。「警告を消して」とだけ頼むと、この方法で済まされることがあります。

直ったかどうかを、同じ手順で確かめる

修正後は、再現時と同じページ・同じ操作でコンソールを読ませます。

修正後の localhost:3000/dashboard を再読み込みして、さっきと同じ条件で
コンソールを確認してください。hydration関連の警告が残っていれば全文を、
なければ「出ていない」とだけ報告してください。

注意したいのは、ローカルで警告が消えても、本番で出なくなったとは限らない点です。ローカルの開発サーバーにはCDNが挟まりません。Cloudflareの自動圧縮のようにHTMLを書き換える機能は、本番でだけ働きます。iOSの電話番号リンク化も、実機のSafariでしか起きません。本番だけで出るときは、コードより先に環境側を疑います。iOS側は、次のmetaタグで自動検出を止められます。

<meta
  name="format-detection"
  content="telephone=no, date=no, email=no, address=no"
/>

同じエラーを繰り返さないために

直したあとは、描画中に使ってはいけないものをCLAUDE.mdに書いておきます。次回以降、Claudeが新しいコンポーネントを書く段階で避けるようになります。

## SSRとハイドレーション
- 描画の本体で Date.now()・new Date()・Math.random() を使わない
- 描画の本体で window・localStorage を読まない。必要なら useEffect で状態に入れる
- 日付表示はロケールを固定した書式で整形する
- suppressHydrationWarning は最終手段。使うときは理由をコメントに書く

これは運用上の一例です。ルールを書くだけでは強制にならないので、Next.jsの開発サーバーで警告が出たら失敗とみなす検証を、完了条件に加えるほうが確実です。完了条件の組み方はClaude Codeのデバッグ手順が参考になります。

Next.jsのプロジェクトをゼロから組む場合は、Claude CodeでNext.js SaaS MVPを作るから入る手もあります。

よくある質問

ブラウザの拡張機能が原因かは、どう見分けますか

拡張機能を無効にするか、拡張機能のない別のプロファイルで同じページを開きます。それで警告が消えれば、コードは関係していません。Reactのエラーページも、HTMLを書き換える拡張機能が原因になりうると説明しています。

Claude in Chrome自体が警告の原因になりますか

Chrome連携のドキュメントには、ページのHTMLを書き換える挙動の説明はありません。疑うなら、Claudeを繋がない通常のウィンドウで同じページを開き、警告が出るか見比べます。

警告を消すだけで済ませてよいですか

suppressHydrationWarningは、食い違いが避けられない1要素に限る逃げ道です。ドキュメント自身が使いすぎないよう注意しています。まず原因を特定し、直せないものにだけ使います。

まとめ

hydrationエラーは、原因はコードの中にも、ブラウザやCDNのような環境側にもあります。Chrome連携でコンソールを読ませれば、警告の対象を特定する作業は任せられます。コードか環境かの切り分けと、「警告を消す」のか「食い違いを直す」のかの判断は、人が持つ部分です。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/chrome
  2. nextjs.org/docs/messages/react-hydration-error
  3. react.dev/errors/418
  4. react.dev/reference/react-dom/client/hydrateRoot
この記事を共有:XはてブLinkedIn