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へ渡す
処理の流れは次のとおりです。
- サーバーがデータを取得する
- データをJSONへ安全にシリアライズする
application/jsonのdata blockへ埋め込む- client entryでJSONを一度読み取る
- 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>
);
}
初期データを同期的に取得できるため、読み取りのためだけにuseEffectとuseStateを使う必要はありません。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_AMP、JSON_HEX_APOS、JSON_HEX_QUOTもHTML上で意味を持ち得る文字をUnicode escapeへ変換します。JSON_THROW_ON_ERRORはUTF-8不正などのエンコード失敗を例外として扱います。
JSON全体へhtmlspecialcharsを使わない
script要素の内容は通常のHTMLテキストと解析方法が異なります。JSON文字列全体をhtmlspecialchars()へ通すと、ダブルクォートが"になり、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でtextContentをJSON.parse()してReactへpropsで渡せます。
PHPではJSON_HEX_TAGなどのflagsとJSON_THROW_ON_ERRORを使い、データ中の</script>がHTMLを途中終了しないようにします。解析後はJSON構造も検証してください。
空のrootへ描画するならcreateRoot、React SSR HTMLへ接続するならhydrateRootをreact-dom/clientから使います。Server Componentsとuse()は対応frameworkが管理する別方式として切り分けます。