🔧 JSXは何に変換されるのか?
JSXはHTMLのように見えるが、実はただのJavaScript関数呼び出しです。 ビルド時に何が起きているのか、旧来のトランスフォームと新しいトランスフォームの違いを深掘りします。
🤔 問題提起
Reactを使い始めたとき、こんな疑問を持ったことはないでしょうか?
- JSXはHTMLに似ているが、なぜTypeScript(JavaScript)ファイルの中に書けるのか?
- ブラウザはJSXを直接理解できないはずなのに、なぜ動くのか?
- React 17以前は
import React from 'react'が必要だったのに、なぜ今は不要なのか? <>...</>というFragmentの書き方は何に変換されるのか?
これらはすべて「JSXのトランスフォーム」という仕組みを理解すれば解決します。 JSXは実はビルドツール(BabelやTypeScriptのコンパイラ)によって、純粋なJavaScript関数呼び出しに変換されます。
🎯 結論から言う
JSXはビルド時に関数呼び出しに変換されます。
React 17以前は React.createElement()、React 17以降は jsx() 関数(react/jsx-runtime)に変換されます。
🗺️ 変換の全体像
JSXが最終的にDOMになるまでの流れを整理します。変換はビルド時(静的)とランタイム(動的)に分かれています。
<div className="foo">Hello</div> jsx("div", { className: "foo" }, "Hello") { type: "div", props: { className: "foo", children: "Hello" }, ... } Fiber { type, pendingProps, memoizedState, child, sibling, ... } <div class="foo">Hello</div> 📜 旧トランスフォーム(React 17以前)
React 16以前のBabelおよびTypeScriptは、JSXをReact.createElement()呼び出しに変換していました。 この変換は「クラシックトランスフォーム」と呼ばれます。
// ファイルの先頭にこのimportが必須だった
import React from 'react';
function Greeting({ name }) {
return (
<div className="greeting">
<h1>Hello, {name}!</h1>
<p>Welcome to React.</p>
</div>
);
} import React from 'react';
function Greeting({ name }) {
return React.createElement(
"div",
{ className: "greeting" },
React.createElement("h1", null, "Hello, ", name, "!"),
React.createElement("p", null, "Welcome to React.")
);
} なぜimport Reactが必要だったのか
旧トランスフォームではJSXがReact.createElement()に変換されます。
つまり変換後のコードにReactという識別子が使われるため、
スコープにReactオブジェクトが存在しなければなりません。
仮にimport React from 'react'を忘れると、
ランタイムエラー「React is not defined」が発生します。
これはJSXをHTMLのように書いている人には非直感的で、よくある初歩的なミスでした。
React.createElement( type, // 'div' などの文字列、またはコンポーネント関数/クラス props, // プロパティのオブジェクト(nullも可) ...children // 子要素(可変長引数) )
第3引数以降が子要素として展開され、propsオブジェクトの
childrenプロパティにまとめられます。
子が1つの場合は直接値、複数の場合は配列になります。
⚡ 新トランスフォーム(React 17以降)
React 17でReactチームはBabelと協力して新しいJSXトランスフォームを導入しました。
これにより、JSXを含むファイルにimport React from 'react'を
書く必要がなくなりました。
// import React from 'react' が不要!
function Greeting({ name }) {
return (
<div className="greeting">
<h1>Hello, {name}!</h1>
<p>Welcome to React.</p>
</div>
);
} // Babelが自動的にimportを挿入する
import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime';
function Greeting({ name }) {
return _jsxs("div", {
className: "greeting",
children: [
_jsx("h1", { children: ["Hello, ", name, "!"] }),
_jsx("p", { children: "Welcome to React." })
]
});
} jsx() と jsxs() の違い
新トランスフォームには2つの関数があります。
jsx(type, props)— 子が1つまたは0個の要素jsxs(type, props)— 子が複数の要素(children配列)jsxDEV()— 開発モード専用(ソース位置情報を含む)
jsxsは主にパフォーマンス最適化のための分岐で、
内部的には同じReactElementオブジェクトを生成します。
新トランスフォームのメリット
- ファイルごとの
import Reactが不要になった - バンドルサイズがわずかに削減(不要なReactオブジェクト参照がなくなる)
- 将来的なJSX改善(Reactに依存しない構文拡張)が容易になる
- 型推論・エラーメッセージが改善された(jsxDEVがより詳細な情報を持つ)
🧱 ReactElementオブジェクトの構造
React.createElement()または
jsx()は、どちらも
ReactElementオブジェクトを返します。
これはただのプレーンなJavaScriptオブジェクトです。
interface ReactElement {
// 要素の種類: 'div', 'span' などのDOM文字列、
// またはコンポーネント関数・クラス
type: string | ComponentType;
// 要素を識別するキー(リスト描画で使用)
key: string | null;
// 参照(useRef / createRef)
ref: Ref | null;
// コンポーネントに渡されるプロパティ
// childrenもここに含まれる
props: {
children?: ReactNode;
[key: string]: any;
};
// XSS対策フラグ(後述)
$$typeof: symbol;
} // <button onClick={handleClick} className="btn">Click me</button>
// が変換されると以下のオブジェクトになる
{
$$typeof: Symbol(react.element), // XSS対策
type: "button",
key: null,
ref: null,
props: {
onClick: handleClick, // 関数参照
className: "btn",
children: "Click me"
}
} $$typeofとXSS防止の仕組み
$$typeof: Symbol(react.element)は
一見地味ですが、XSS(クロスサイトスクリプティング)攻撃を防ぐための重要な安全機構です。
攻撃シナリオを考えます:サーバーからJSONでデータを受け取り、そのデータをそのままReactに渡す場合、
攻撃者がJSON内にReactElementのような構造({ type: 'script', props: { dangerouslySetInnerHTML: ... } })を
埋め込む可能性があります。
しかしSymbolはJSONにシリアライズできません(JSONにはSymbol型が存在しない)。
そのため、サーバーからのJSONにはこのシンボルが含まれず、Reactは「これは正規のReactElementではない」と判断して拒否します。
🧩 コンポーネント呼び出しの変換
DOM要素だけでなく、Reactコンポーネントもまったく同じ方法で変換されます。
違いはtypeが文字列ではなく関数参照になる点です。
// JSX(変換前)
function App() {
return (
<UserCard
userId={42}
isAdmin={true}
onLogout={() => console.log('logout')}
/>
);
}
// 変換後(新トランスフォーム)
import { jsx as _jsx } from 'react/jsx-runtime';
function App() {
return _jsx(UserCard, { // ← 文字列ではなく関数参照!
userId: 42,
isAdmin: true,
onLogout: () => console.log('logout')
});
}
// 生成されるReactElement
{
$$typeof: Symbol(react.element),
type: UserCard, // ← 関数自体への参照
props: {
userId: 42,
isAdmin: true,
onLogout: [Function]
},
key: null,
ref: null
}
Reactはこのオブジェクトを受け取ると、typeが関数であることを確認し、
その関数をpropsを引数として呼び出します。
これがコンポーネントのレンダリングの本質です。
🔗 Fragmentの変換
<>...</>という書き方(Fragment短縮構文)も
同様に変換されます。React.Fragmentという特殊な型を使います。
// 変換前(JSX)
function List() {
return (
<>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</>
);
}
// 変換後(新トランスフォーム)
import { jsxs as _jsxs, Fragment as _Fragment } from 'react/jsx-runtime';
function List() {
return _jsxs(_Fragment, {
children: [
_jsx("li", { children: "Item 1" }),
_jsx("li", { children: "Item 2" }),
_jsx("li", { children: "Item 3" })
]
});
}
// <React.Fragment key="myKey"> という明示的な書き方の変換
// _jsxs(Fragment, { children: [...] }, "myKey") Fragmentが存在する理由
Reactのコンポーネントは単一のルート要素を返す必要があります(JSXの制約)。 しかしDOMに不要なラッパー要素を追加したくない場合に、 Fragmentを使うことで複数の要素をグループ化できます。 Fragmentはレンダリング時に実際のDOMノードを生成しません。
🔍 具体的なコード例で確認する
より複雑なJSXの変換を確認してみましょう。条件分岐、リスト、イベントハンドラーを含む実用的な例です。
function TodoList({ todos, onDelete }) {
if (todos.length === 0) {
return <p className="empty">タスクはありません</p>;
}
return (
<ul className="todo-list">
{todos.map((todo) => (
<li key={todo.id} className={todo.done ? 'done' : ''}>
<span>{todo.title}</span>
<button onClick={() => onDelete(todo.id)}>削除</button>
</li>
))}
</ul>
);
} import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime';
function TodoList({ todos, onDelete }) {
if (todos.length === 0) {
return _jsx("p", { className: "empty", children: "タスクはありません" });
}
return _jsx("ul", {
className: "todo-list",
children: todos.map((todo) =>
_jsxs("li", {
className: todo.done ? 'done' : '',
children: [
_jsx("span", { children: todo.title }),
_jsx("button", {
onClick: () => onDelete(todo.id),
children: "削除"
})
]
}, todo.id) // ← key は第3引数として渡される
)
});
} keyの扱いに注目
keyプロパティは特別な扱いを受けます。
propsオブジェクトには含まれず、jsx/jsxs関数の第3引数として渡されます。
これはReactが内部的にkeyをpropsとは区別して管理するためです。
コンポーネント内でprops.keyにアクセスするとundefinedになります。
⚙️ 仕組みの分解:JSXは「評価される式」
JSXが関数呼び出しに変換されるということは、JSXは式(expression)として評価されます。 つまり、変数に代入したり、条件演算子の結果として使ったり、配列の要素にすることができます。
// ✅ 変数への代入(JSXは関数の戻り値なので代入可能)
const element = <div>Hello</div>;
// ✅ 三項演算子(どちらもReactElementを返す)
const button = isLoading
? <Spinner />
: <button>Submit</button>;
// ✅ 配列の要素(mapで生成するリスト)
const items = todos.map(t => <li key={t.id}>{t.title}</li>);
// ✅ 関数の引数(高階コンポーネントのパターン)
renderModal(<ConfirmDialog message="Are you sure?" />);
// ⚠️ JSXを返す関数は必ずReactElementを返している
function renderIcon(name) {
switch(name) {
case 'check': return <CheckIcon />;
case 'x': return <XIcon />;
default: return null; // nullは有効な戻り値
}
} JSXとReactは独立している
JSXはReact専用の構文ではありません。BabelやTypeScriptの
jsxImportSource設定を変更することで、
JSXを異なるランタイム(PreactやSolidJSなど)の関数呼び出しに変換できます。
// tsconfig.json でSolidJSのランタイムを使う例
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "solid-js/h" // solid-jsのjsx関数を使用
}
}
// Preactの場合
/** @jsxImportSource preact */
// このコメントで特定ファイルだけランタイムを切り替えられる 📌 まとめ
- ✓ JSXはブラウザが理解できる構文ではなく、ビルド時にJavaScript関数呼び出しに変換される
- ✓ 旧トランスフォーム(React 17以前)はReact.createElement()に変換——importが必須だった
- ✓ 新トランスフォーム(React 17以降)はjsx/jsxs関数に変換——importはBabelが自動挿入
- ✓ 変換結果はReactElementオブジェクト——typeとpropsを持つプレーンなJSオブジェクト
- ✓ $$typeofシンボルはXSS対策の安全機構——JSON経由の偽造ReactElementを防ぐ
- ✓ keyはpropsではなく第3引数——コンポーネント内でprops.keyにはアクセスできない
- ✓ JSXはReact専用ではない——jsxImportSourceで別ランタイムに向けることができる