JSXがどのように変換されて最終的に画面上に表示されるか、最小限の構成をその仕組みを学習してみたので、書き残しておきます。
「この記述はなんとなくこうなっているのだろう...」という曖昧な理解のまま放置しておかないように「なんとなく理解禁止」というシリーズでJavaScriptやReactの学習記録を残していて、誰か1人にでも参考になれば嬉しいです...!
JSXとは
JSX(JavaScript XML)とは、HTMLのような書き方でJavaScriptソースを記述できるようにした Meta(旧Facebook)が開発した構文です。
例えば、下記のようにシンプルな見出し要素を記述できます。
const component = <h1>Hello</h1>;
ちなみにこのHTMLタグ等はHTMLではなく、全てJavaScriptのソースです。なので、下記のようにJavaScriptのコードを実行するための印(中括弧{})の中であれば、計算結果を書くこともできます。
const myname = 'Tsukasa';
const component = <h1>Hello {myname}!</h1>;
// componentが出力するHTML結果:<h1>Hello Tsukasa!</h1>
JSXがReact要素に変換されてレンダリングされるまでの流れ
下記の記述でH1タグでHello Worldを画面上に表示できます。
<div id="app"></div>
const root = createRoot(document.getElementById('app'));
root.render(<h1>Hello, world!</h1>);
上記の記述を2つに因数分解すると、ざっと下記の流れになります。
- babel等を使ってJSX構文をReact要素に変換
- createRootのrenderメソッドでReact要素をHTMLとして挿入
① babel等を使ってJSXをReact要素に変換
const component = <h1>Hello, world!</h1>;
上記のようなJSXで書いた構文は、babel等のコンパイラを使って下記のようなReact.createElement()
メソッドを使ったJavaScript形式のソースに変換されます。
const component = React.createElement("h1", null, "Hello, world!");
前者と後者の実行結果は同じで、JSXはReact.createElement()
の記述のシンタックスシュガーということになります。
シンタックスシュガー(英:syntax sugar)とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
② createRoot().renderメソッドでReact要素をHTMLとして挿入
あとは簡単で、React要素に変換したオブジェクトを材料に、createRoot().renderメソッドを使ってHTMLとして挿入します。
<div id="app"></div>
// HTMLを挿入するルートを特定
const root = createRoot(document.getElementById('app'));
// renderメソッドを使ってReact要素をルートに挿入
root.render(React.createElement("h1", null, "Hello, world!"));
初めは下記のようにrenderメソッドの引数に直接JSX構文を書いていましたが、上記のコードでも下記でも実行結果は同じです。
const root = createRoot(document.getElementById('app'));
root.render(<h1>Hello, world!</h1>);
内部的に、<h1>Hello, world!</h1>
というJSX構文をReact.createElement()
に変換していた訳です。
補足
ちなみに、React.createElement()
の戻り値をコンソールで確認すると、下記のようなオブジェクトを確認できます。
▼object
$$typeof: Symbol(react.element)
key: null
props: {children: 'Hello, world!'}
ref: null
type: "h1"
_owner: null
React.createElement()
の3つの引数がそれぞれ代入されていて...
React.createElement()
の第一引数がtype
(HTMLタグ)React.createElement()
の第二引数がprops
(属性値)React.createElement()
の第三引数がprops.children
(コンポーネントの子要素)
といった形で割り当てられて、このオブジェクトを使ってHTMLタグを作って挿入しているイメージになります。
余談ですが、こうして一つひとつ理解していくとJSXは結局ただのJavaScriptのオブジェクトだという事が分かります。JSX構文とReact.createElement()
は書き方が違うだけで機能的には同じで、React.createElement()
が返すのは普通のJavaScriptなので、JSXは色々変換された結果、最終的にはJavaScriptのオブジェクトとなります。
実際にJSXがレンダリングされるまでの流れを最小限の構成で試してみる
下記が今回の最小限の構成です。
dist/
├── assets/
└── index.html
src/
├── index.html
└── main.js
babel.config.json
package.json
vite.config.js
扱っている環境やツールは下記の通り。
- vite(reactファイルのバンドルのためだけに使用)
- babel(JSXからReact要素へのコンパイルのために使用)
- yarn(パッケージマネージャー、もちろんnpmでもOK)
学習のためにこのような環境にしましたが、普通はNext.jsやRemix等のReactセットアップコマンドやViteのReactテンプレートのセットアップコマンドを叩けば簡単にReactを試せます。
1/2|Viteでバンドルするための環境を整える
Viteセットアップ時にReactのセットアップテンプレートは使わず、Vanillaを選択してフラットな環境にします。
yarn create vite
# Select a framework:
# Vanillaを選択
# Select a variant:
# JavaScriptを選択
cd vite-project
説明に不要なファイルは省きますが、セットアップ後は下記のような構成になります。
vite-project/
├── counter.js
├── index.html
├── main.js
└── package.json
これでyarn build
を叩けば、index.html
をエントリーポイントにしてmain.js
のバンドルもした上でプロジェクトルートにdist
が吐き出されます。
2/2|JSX構文をReact要素に変換するためのbabel環境を整える
babelの最小限のセットアップ方法は公式ガイドを参照すれば理解できます。
# 必要なパッケージをインストール
yarn add --dev @babel/core @babel/cli @babel/preset-react
インストール後、babelの設定ファイルをプロジェクトルートに設置。
//
{
"presets": [
"@babel/preset-react"
]
}
package.jsonのscripts.buildにviteも含めたコマンドを設定します。
{
"scripts": {
"dev": "vite",
"build": "./node_modules/.bin/babel ./src/main.js --out-file ./src/main.babel.js && vite build",
"preview": "vite preview"
},
"devDependencies": {
"@babel/cli": "^7.24.8",
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/preset-react": "^7.24.7",
"vite": "^5.4.0"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
}
./node_modules/.bin/babel ./src/main.js --out-file ./src/main.babel.js
の記述が、babelを使って./src/main.js
を./src/main.babel.js
に吐き出すコマンドになります。
babel実行前の./src/main.js
と実行後の./src/main.babel.js
は下記の通り。
import './style.css'
import ReactDOM from 'react-dom';
import React from 'react';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('app'));
root.render(<h1>Hello</h1>);
import './style.css';
import ReactDOM from 'react-dom';
import React from 'react';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('app'));
root.render( /*#__PURE__*/React.createElement("h1", null, "Hello"));
root.render()
の引数が<h1>Hello</h1>
のJSX構文からReact.createElement("h1", null, "Hello")
のReact要素に変換されていることが分かります。
あとは、後続のvite build
コマンドによって、src/index.html
で挿入している<script src="./main.babel.js" type="module"></script>
を読んで、main.babel.js
のインポートされているパッケージを全てバンドルしてくれます。
ビルド前の`index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script src="./main.babel.js" type="module"></script>
</body>
</html>
ビルド後の`index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<script type="module" crossorigin src="./assets/index-CTqbuTW-.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BfibREyH.css">
</head>
<body>
<div id="app"><!-- ./assets/index-CTqbuTW-.jsによって<h1>Hello</h1>が挿入される --></div>
</body>
</html>