React

ReactでHTML埋め込みJSONを取得する方法|PHP連携とXSS対策

PHPなどでHTMLへ埋め込んだJSONをReactの初期データとして取得する方法を解説します。application/json、JSON.parse、createRoot・hydrateRoot、Strict Mode、Server Componentsとの違い、script終了文字列によるXSSを防ぐエンコードまで紹介します。

この記事の目次
  1. HTMLへJSONを埋め込む方法の全体像
  2. 静的HTMLへJSONを埋め込む
  3. JSONを安全に読み取る関数を作る
  4. 読み取り後にscript要素を削除するか
  5. Reactへ初期データをpropsで渡す
  6. 取得後にデータ構造を検証する
  7. PHPからJSONを安全に埋め込む
  8. JSON全体へhtmlspecialcharsを使わない
  9. なぜjson_encodeだけでは不十分なのか
  10. React Strict Modeとの関係
  11. createRootとhydrateRootの使い分け
  12. root内が空ならcreateRoot
  13. root内がReactのSSR HTMLならhydrateRoot
  14. Server Componentsとuseは別の方式
  15. metaタグやdata属性へJSONを入れるべきか
  16. 埋め込みJSONとAPI取得の使い分け
  17. 埋め込みJSONが向く場合
  18. API fetchが向く場合
  19. 埋め込まない方がよいデータ
  20. よくあるエラーと確認方法
  21. Unexpected token in JSON
  22. Cannot read properties of null
  23. categories.map is not a function
  24. Hydration failed
  25. 開発環境でconsole.logが2回出る
  26. よくある質問
  27. application/jsonのscriptは実行されますか?
  28. JSONの読み取りにカスタムHookは必要ですか?
  29. Strict Mode対策としてuseRefへ保存すればよいですか?
  30. JSONをwindow.__INITIAL_DATA__へ代入してもよいですか?
  31. 埋め込みJSONはSEOに効果がありますか?
  32. まとめ

PHPなどのサーバーテンプレートで生成したJSONを、Reactの初期データとして使う場合は、type="application/json"のscript要素へ埋め込みます。

<div id="root"></div>

<script id="initial-data" type="application/json">
  {"user":{"id":1,"name":"山田"}}
</script>

<script type="module" src="/src/main.jsx"></script>

JavaScriptでtextContentを読み、JSON.parse()した値をReact rootへpropsとして渡します。

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";

const initialData = readInitialData("initial-data");

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <App initialData={initialData} />
  </StrictMode>,
);

ただし、サーバー値をjson_encode()しただけでHTMLへ出すと、データに</script>が含まれる場合にscript要素を途中で閉じられる危険があります。埋め込み側の安全なシリアライズが重要です。

HTMLへJSONを埋め込む方法の全体像

この方法は、次のような構成に向いています。

  • 既存のPHP、Ruby、CMSテンプレートの一部へReactを追加する
  • 初期表示に必要なデータを追加のAPIリクエストなしで渡す
  • サーバー側で権限確認・取得済みのデータをclient appへ渡す
  • ページごとに異なる設定や一覧データをReactへ渡す

処理の流れは次のとおりです。

  1. サーバーがデータを取得する
  2. データをJSONへ安全にシリアライズする
  3. application/jsonのdata blockへ埋め込む
  4. client entryでJSONを一度読み取る
  5. Reactコンポーネントへpropsとして渡す

application/jsonはJavaScriptのMIME typeではないため、ブラウザはscript内容をコードとして実行せずdata blockとして扱います。ただし、HTML parserがscript終了タグを認識する問題は別に対策が必要です。

静的HTMLへJSONを埋め込む

まず固定データで構造を確認します。

<div id="root"></div>

<script id="catalog-data" type="application/json">
{
  "categories": [
    {
      "id": 1,
      "name": "総合カタログ",
      "items": [
        {
          "id": 101,
          "title": "2026年版カタログ",
          "imageUrl": "/assets/catalog-2026.jpg"
        }
      ]
    }
  ]
}
</script>

<script type="module" src="/src/main.jsx"></script>

JSONはJavaScript object literalとは違います。キーと文字列はダブルクォートで囲み、末尾カンマ、コメント、undefinedなどを入れません。

JSONを安全に読み取る関数を作る

DOM要素がない場合、JSONが空の場合、構文が壊れている場合を区別します。

export function readInitialData(elementId) {
  const element = document.getElementById(elementId);

  if (!element) {
    throw new Error(`#${elementId} が見つかりません`);
  }

  const json = element.textContent?.trim();

  if (!json) {
    throw new Error(`#${elementId} のJSONが空です`);
  }

  try {
    return JSON.parse(json);
  } catch (error) {
    throw new Error(
      `初期JSONの解析に失敗しました: ${error.message}`,
      { cause: error },
    );
  }
}

eval()new Function()でJSONを評価してはいけません。JSON文字列はJSON.parse()でデータとして解析します。

読み取り後にscript要素を削除するか

アプリ内で再参照しないなら、解析後にdata blockをDOMから削除できます。

const dataElement = document.getElementById("catalog-data");
const catalogData = readInitialData("catalog-data");

dataElement?.remove();

削除してもJavaScript objectとして保持したデータは利用できます。ただし、別のscriptも同じdata blockを読む設計なら削除しません。

Reactへ初期データをpropsで渡す

JSONの読み取りは、コンポーネントの外側にあるclient entryで行います。

// main.jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import { readInitialData } from "./readInitialData.js";

const rootElement = document.getElementById("root");

if (!rootElement) {
  throw new Error("#root が見つかりません");
}

const initialData = readInitialData("catalog-data");

createRoot(rootElement).render(
  <StrictMode>
    <App initialData={initialData} />
  </StrictMode>,
);

Appはpropsを受け取り、必要なコンポーネントへ渡します。

// App.jsx
export default function App({ initialData }) {
  return (
    <main>
      <h1>カタログ一覧</h1>

      {initialData.categories.map((category) => (
        <section key={category.id}>
          <h2>{category.name}</h2>

          <ul>
            {category.items.map((item) => (
              <li key={item.id}>
                <img src={item.imageUrl} alt="" />
                <span>{item.title}</span>
              </li>
            ))}
          </ul>
        </section>
      ))}
    </main>
  );
}

初期データを同期的に取得できるため、読み取りのためだけにuseEffectuseStateを使う必要はありません。Effectへ移すと、最初のrenderではデータがなく、後から再renderする構成になります。

取得後にデータ構造を検証する

JSONとして正しくても、アプリが期待する構造とは限りません。配列、object、必須キー、各値の型を確認します。

function isCatalogData(value) {
  if (!value || typeof value !== "object") {
    return false;
  }

  if (!Array.isArray(value.categories)) {
    return false;
  }

  return value.categories.every((category) => {
    return (
      category
      && typeof category === "object"
      && Number.isInteger(category.id)
      && typeof category.name === "string"
      && Array.isArray(category.items)
    );
  });
}

const initialData = readInitialData("catalog-data");

if (!isCatalogData(initialData)) {
  throw new Error("カタログデータの形式が正しくありません");
}

大規模アプリではschema validation libraryを使う方法もあります。重要なのは、TypeScriptの型注釈だけでは実行時に届く値を検証できない点です。

PHPからJSONを安全に埋め込む

PHPで用意した配列をjson_encode()し、data blockへ出力します。

<?php
$catalogData = [
    'categories' => [
        [
            'id' => 1,
            'name' => '総合カタログ',
            'items' => [
                [
                    'id' => 101,
                    'title' => '2026年版カタログ',
                    'imageUrl' => '/assets/catalog-2026.jpg',
                ],
            ],
        ],
    ],
];

$jsonFlags = JSON_HEX_TAG
    | JSON_HEX_AMP
    | JSON_HEX_APOS
    | JSON_HEX_QUOT
    | JSON_UNESCAPED_UNICODE
    | JSON_THROW_ON_ERROR;
?>

<div id="root"></div>

<script id="catalog-data" type="application/json">
<?= json_encode($catalogData, $jsonFlags) ?>
</script>

<script type="module" src="/assets/main.js"></script>

JSON_HEX_TAG<>をUnicode escapeへ変換します。これによりデータ中の</script>がHTML parserから終了タグとして見えなくなります。

JSON_HEX_AMPJSON_HEX_APOSJSON_HEX_QUOTもHTML上で意味を持ち得る文字をUnicode escapeへ変換します。JSON_THROW_ON_ERRORはUTF-8不正などのエンコード失敗を例外として扱います。

JSON全体へhtmlspecialcharsを使わない

script要素の内容は通常のHTMLテキストと解析方法が異なります。JSON文字列全体をhtmlspecialchars()へ通すと、ダブルクォートが&quot;になり、JSON.parse()できない文字列になることがあります。

通常のHTML本文や属性へ値を出す場合と、script data blockへJSONを出す場合でエンコードを使い分けます。

なぜjson_encodeだけでは不十分なのか

データベースに次の文字列が入っている場合を考えます。

</script><img src=x onerror=alert(1)>

JSON自体としては有効な文字列でも、そのままHTMLのscript要素内へ入れるとHTML parserが</script>を終了タグとして解釈できます。scriptのtypeがapplication/jsonでも、終了タグ問題が自動で消えるわけではありません。

PHP例のJSON_HEX_TAGを使うと、先頭の<\u003Cとして出力され、JSON.parse後に元の文字列へ戻ります。

クライアント側でJSON.stringify()した文字列をサーバーテンプレートへそのまま貼る方法も安全とは限りません。シリアライズ結果をどのHTML contextへ出すかに合わせた対策が必要です。

React Strict Modeとの関係

Strict Modeは、React treeを<StrictMode>で囲むか、framework側で有効にした場合に動作します。「React 19なら常に自動で有効」という機能ではありません。

有効な開発環境では、純粋でないrender処理を見つけるためコンポーネント関数が追加で呼ばれ、Effectも追加のsetup・cleanup cycleを実行します。production buildにはこの追加チェックはありません。

JSONの読み取りをclient entryのmodule top-levelで行い、解析済みobjectをpropsへ渡せば、コンポーネントrenderのたびにDOM検索とJSON.parseを繰り返しません。

// React componentの外で一度読み取る
const initialData = readInitialData("catalog-data");

createRoot(rootElement).render(
  <StrictMode>
    <App initialData={initialData} />
  </StrictMode>,
);

useRefを使えばStrict Modeによる追加render自体を止められるわけではありません。renderは副作用を持たず、同じpropsなら同じJSXを返すように作ります。

createRootとhydrateRootの使い分け

root内が空ならcreateRoot

PHPがroot要素だけを出し、Reactがブラウザで初めてUIを描画する場合はcreateRootを使います。

import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")).render(
  <App initialData={initialData} />,
);

root内がReactのSSR HTMLならhydrateRoot

root内のHTMLがreact-dom/serverで生成されている場合は、既存HTMLへイベント処理を接続するhydrateRootを使います。

import { hydrateRoot } from "react-dom/client";

hydrateRoot(
  document.getElementById("root"),
  <App initialData={initialData} />,
);

hydrate時の最初のclient renderは、serverで生成したHTMLと一致する必要があります。server renderに使ったデータと同じsnapshotをdata blockで渡し、client側でも同じ値を使います。

PHPが手書きした似たHTMLへReactを接続するためにhydrateRootを使うものではありません。React SSRではないHTMLならcreateRootで別のrootへ描画します。

Server Componentsとuseは別の方式

React Server Componentsは、対応するbundlerやframeworkがserver/client境界、serialization、routing、data transportを構築する仕組みです。通常のVite client appへ.server.jsxをimportするだけでは成立しません。

Server Componentはasync functionとしてデータをawaitできます。また、serverで作成したPromiseをClient Componentへ渡し、Client ComponentがReact 19のuse()で読む構成もあります。

// Server Componentの概念例
export default async function CatalogPage() {
  const catalogData = await getCatalogData();

  return <CatalogList catalogData={catalogData} />;
}

これはframeworkが管理するRSC構成の例です。PHPで生成したHTMLへ小さなReact appを載せる目的なら、data blockからpropsへ渡す方法の方が単純です。

Client Componentのrender内で毎回use(fetch(...))のように新しいPromiseを作るのは避け、利用するframeworkのdata fetchingとcacheの規約に従います。

metaタグやdata属性へJSONを入れるべきか

短い単一値ならdata属性を使えます。

<div id="root" data-locale="ja" data-theme="dark"></div>

しかし、入れ子のあるJSONをmetaのcontent属性やdata属性へ入れると、JSONのダブルクォートとHTML属性のエスケープが重なり、読み書きしにくくなります。

用途を整理すると次のようになります。

データ 方法
locale、theme、IDなど短い値 data属性
object・arrayの初期データ application/json data block
大きい・頻繁に更新されるデータ APIからfetch
対応frameworkのserver data frameworkのRSC / SSR機能

meta要素はページmetadataを表す用途が中心です。アプリケーション用の大きなJSON保存先として選ぶ利点はありません。

埋め込みJSONとAPI取得の使い分け

埋め込みJSONが向く場合

  • 初期renderに必須で、HTML生成時点に値が確定している
  • ページごとに一度だけ使う
  • 小〜中規模でHTMLサイズを過度に増やさない
  • APIへの追加round tripを避けたい

API fetchが向く場合

  • ページ表示後も更新される
  • ユーザー操作後にだけ必要
  • データ量が大きい
  • 独立したcache・再検証・エラー処理が必要

APIから取得する実装はReactで外部APIのJSONを取得して表示する方法、fetch自体の基本はJavaScriptのfetchとJSONで解説しています。

埋め込まない方がよいデータ

HTML sourceは利用者から見えます。次の値を秘密情報として埋め込んではいけません。

  • API secret、database password、private key
  • 他ユーザーの非公開データ
  • serverだけで使う認可情報
  • 権限判定の根拠となる改ざん可能なclient値

clientへ必要な公開可能データだけを選び、server側で認可した後に渡します。React propsへ渡した値もbrowserから確認・変更できる前提です。

よくあるエラーと確認方法

Unexpected token in JSON

JSONに末尾カンマ、コメント、シングルクォート、HTML entityなどが入っていないか確認します。data blockのtextContentをconsoleで確認し、server側のjson_encode失敗も調べます。

Cannot read properties of null

JSON要素またはroot要素が見つかっていません。idの一致と、module scriptが対象HTMLより後にあるかを確認します。

categories.map is not a function

期待したarrayではありません。JSON.parse成功だけで終わらず、Array.isArray(initialData.categories)を確認します。

Hydration failed

server HTMLと最初のclient renderが一致していません。serverとclientで同じ初期データを使っているか、render中のDate.now()Math.random()、locale依存format、browser条件分岐がないか確認します。

開発環境でconsole.logが2回出る

Strict Modeを有効にしている場合、開発専用チェックでコンポーネントが追加renderされることがあります。productionで同じ追加renderが行われるという意味ではありません。render内の処理を純粋にし、Effectにはcleanupを用意します。

よくある質問

application/jsonのscriptは実行されますか?

JavaScript MIME typeではないため、内容はdata blockとして扱われ、コードとして実行されません。ただし、HTMLへ安全に埋め込むための終了タグ対策は必要です。

JSONの読み取りにカスタムHookは必要ですか?

必須ではありません。初期データはclient entryで一度読み、propsまたはContextへ渡す方が役割を分けやすくなります。Hookにする場合もrenderごとにDOMを検索する必要はありません。

Strict Mode対策としてuseRefへ保存すればよいですか?

useRefはcomponent instance内で値を保持しますが、Strict Modeの追加renderを無効にする機能ではありません。解析をcomponent外へ移し、renderを純粋に保ちます。

JSONをwindow.__INITIAL_DATA__へ代入してもよいですか?

実装はできますが、inline executable scriptとなり、global汚染、CSP、JavaScript contextのエンコードを考える必要があります。data blockと外部module scriptに分けると、データと実行コードの境界が明確です。

埋め込みJSONはSEOに効果がありますか?

data block自体は利用者へ表示する本文ではありません。検索対象として必要な内容は、server rendered HTMLまたは通常のページコンテンツとして出します。構造化データとして使うapplication/ld+jsonとは目的が異なります。

まとめ

PHPなどで生成した初期データは、type="application/json"のscript data blockへ埋め込み、client entryでtextContentJSON.parse()してReactへpropsで渡せます。

PHPではJSON_HEX_TAGなどのflagsとJSON_THROW_ON_ERRORを使い、データ中の</script>がHTMLを途中終了しないようにします。解析後はJSON構造も検証してください。

空のrootへ描画するならcreateRoot、React SSR HTMLへ接続するならhydrateRootreact-dom/clientから使います。Server Componentsとuse()は対応frameworkが管理する別方式として切り分けます。