-- Views
June 15, 26
スライド概要
React等フレームワークを使わず、自力でJSXをDOMに変換してみます。
平成のjQueryに令和の JSXを $('ul').append(<li>追加リスト</li>) 2026/06/15 俺たちの勉強会 #6 #orestudy
自己紹介: murasuke murasuke(むらすけ) 株式会社ツールラボ 開発部 部長 SE/プログラマーとして20年、多種多様なシステムを構築 プログラムが書きたいので転職しました (現職) 1年前、人生初のイベント参加(PHPカンファレンス名古屋)を きっかけにして、フロカン名古屋で初登壇 してきました!
はじめに ● 皆さん、 jQuery使ってますか? ● JSX書いてますか? ● 両方同時に使ったことある (酔狂な)人いますか!!?? ご存じの通り、ReactとjQueryは相性が良くありません。 ReactはVirtual DOMを管理し、jQueryは直接DOMを操作するためです。 render()のタイミングでDOMが再生成され、jQueryのコードは消えてしまいます。
JSXはVirtual DOMを作るための仕組みではない JSXは単なる関数呼び出しなので、 その関数が Virtual DOM を返すか、 実DOMを返すかは実装次第です。 そこで今回は jQueryから操作可能な、実 DOMを生成する JSX という、「いいとこどり?」をやってみます! React等フレームワーク は使いません。 実DOMを返す「jsx-runtime」を自作して、JSXを自らの手 に取り戻しましょう!
JSX 基礎
JSXは「HTMLっぽい見た目をした、関数呼び出し(シンタックスシュガー)」です。
JSX はJavaScriptやブラウザの機能ではありません。
TypeScriptやBabel によって、ただの「関数呼び出し 」に変換されます。
例えば、このような感じです。
<!-- JSX -->
h(
<div className="box">
"div",
<h1>Hello</h1>
{ className: "box" },
</div>
h("h1", null, "Hello")
);
続いて「h(tag, props, childlen)」でDOMを生成して返す処理を実装していきます。
JSX 基礎
旧方式( classic runtime)
jsxFactoryで指定した関数に変換されます(cf. React.createElement())
h(
{
"jsx": "react",
"div",
"jsxFactory": "h"
{ className: "box" },
}
h("h1", null, "Hello")
);
新方式( automatic runtime)
TypeScriptがjsx-runtimeのimportを自動で挿入します
import { jsx as _jsx } from ./runtime/jsx-runtime";
{
"jsx": "react-jsx",
"jsxImportSource": "./runtime"
}
_jsx("div", {
className: "box",
children: _jsx("h1", {children: "Hello"})
});
多少の違いはありますが、本質的には変わりありません
JSXから実DOMへの変換
それでは関数 「h(tag, props, children)」 を定義し、
実DOMを生成する処理を実装して行きましょう!
<Border>
{counter}
<button style={{ color: 'blue', margin: '5px' }} onclick={handleInc}>Increment</button>
</Border>
① JSX ⇒ 関数への変換(トランスパイル)
② h(tag, props, children) 関数を実行して DOMを生成する
<div style="border: 1px solid black; padding: 5px; display: inline-block;">
1
<button style="color: blue; margin: 5px;" onclick="handleInc()">Increment</button>
</div>
1. タグ名が関数なら実行
大文字で始まるタグはFunctionに変換されるので、引数(props, children)をつけてそのま
ま呼び出します。
/* function h(tag: any, props: any, ...children: any[]) { */
if (typeof tag === 'function') {
return tag({
...(props || {}),
children,
});
}
※大文字で始まる「タグ名」に対応する「関数コンポーネント」は別途
個別に実装します。
2. 普通のタグなら Element 作成 関数コンポーネント以外は、そのままElementを作成します。 /* function h(tag: any, props: any, ...children: any[]) { */ // elementを作成 const elm = document.createElement(tag);
3. props を属性へ変換
`on`から始まり、値が関数 の場合addEventListener()でイベントに紐づけます
if (key.startsWith('on') && typeof value === 'function') {
const eventName = key.slice(2).toLowerCase();
elm.addEventListener(eventName, value);
}
`style`の場合は、elementのstyleにマージ、`className`なら「class」として追加
if (key === 'style' && typeof value === 'object') {
Object.assign(elm.style, value);
} else if (key === 'className') {
elm.setAttribute('class', value);
} else if (value != null && value !== false) {
// 上記以外の属性を追加
elm.setAttribute(key, String(value));
}
余談:JSX的には「className」ではなく「class」でも大丈夫です (Reactの慣例に従っている )
4. children を append JSXでは`children`に下記を含むことができます ● Node、 文字列、 配列、null、 boolean <div> <div>hello123</div> hello {[1,2,3]} </div> そのため種類ごとに処理を分ける必要があります 配列:再帰的に`appendChildren()`を適用 boolean:処理をスキップ(追加しない) Node:そのまま`appendChild()` 文字列:createTextNode()でNodeを作成してから追加
4. children を append
function appendChildren(parent: Node, children: any) {
if (Array.isArray(children)) {
children.forEach((child) => appendChildren(parent, child));
return;
}
if (children == null || children === false || children === true) {
return;
}
if (children instanceof Node) {
parent.appendChild(children);
return;
}
parent.appendChild(document.createTextNode(String(children)));
}
種類毎に処理を分けて、追加していきます
ランタイム全体像
export function h(tag: any, props: any, ...children: any[]) {
} else if (key === 'style' && typeof value === 'object') {
if (typeof tag === 'function') {
// styleの追加
// 大文字で始まるタグ(関数コンポーネント)を実行
Object.assign(elm.style, value);
return tag({
} else if (key === 'className') {
...(props || {}),
// classの追加
children,
elm.setAttribute('class', value);
});
} else if (value != null && value !== false) {
}
// 上記以外の属性を追加
// elementを作成
elm.setAttribute(key, String(value));
const elm = document.createElement(tag);
}
}
// 属性を追加
}
if (props) {
for (const [key, value] of Object.entries(props)) {
// 子要素の追加
if (key.startsWith('on') && typeof value === 'function') {
appendChildren(elm, children);
// イベントハンドラの追加
return elm;
const eventName = key.slice(2).toLowerCase();
elm.addEventListener(eventName, value);
}
jQuery と組み合わせる ここでようやく本題です。 この「 h()」が返すのは「本物のDOM (HTMLElement)」です。 なので普通に 「jQuery」に渡せます。 $('ul').append(<li>追加リスト</li>); ついに「 jQueryとJSX」が、手を繋ぎあってくれました!
型安全の恩恵も受けられる 例えば、jQueryでは単に文字列なのでタイプミスを検出できませんが、 でも、JSXならTypeScriptがエラー(タイプミス)を検出してくれます。 $('#app').append(<div clasName="box"> as any); さらにこのあたりも利用できるようになります。 ● 補完 ● lint ただし、jQueryはTypeScriptの型定義と相性が悪く、エラーだらけになるので jQuery側は「any」を多用することになります。 余談:jQueryなのにビルドが必要という時点で、あまり実用的ではないですよね・・・
動作サンプル
「Increment」ボタンをクリックすると+1するカウンターコンポーネント
export type Child = | Node | string | number | boolean
| null | undefined | Child[];
export type Props = {
style?: Partial<CSSStyleDeclaration>;
children?: Child;
型定義
Child:Node、文字、自身の配列
Props:css定義、Child以外は任意
[key: string]: unknown;
};
function Border({ children, ...props }: Props) {
return (
<div {...props}
style={{
border: '1px solid black', padding: '5px',
display: 'inline-block',
}}
> {children} </div>
);
}
Border
childrenに枠をつけるコンポーネント
動作サンプル
「Increment」ボタンをクリックすると+1するカウンターコンポーネント
let count = 0;
const counter = (<div>Count: {count}</div>) as HTMLDivElement;
const handleInc = () => {
counter.innerText = `Count: ${++count}`;
};
const elements = (
<Border>
{counter}
<button id="increment-button"
onclick={handleInc}>Increment</button>
</Border>
);
$('#app').append(elements as any);
$('#increment-button').css('color', 'blue').css('margin', '5px');
●
<Border>による枠の追加
●
jQueryによるDOM追加
●
onclickによるイベントハンドラ
●
jQueryによるスタイル定義
ともに想定した通りに動きました!
まとめ 今回は 「jsx-runtime を自作し、 jQuery と JSX を共存させる」 と言う話をしました。 ● JSX はただの関数呼び出し ● コンポーネントは関数 ● JSX は自分で作れる ● 実DOMを返せば jQuery と共存可能 自作「 jsx-runtime」を通して、ReactやJSXの仕組みを「チョットダケ 」 理解できたのではないでしょうか?