jsx は React 用というイメージがあるが、設定次第で React 以外でも利用でき、「JavaScript/TypeScript といっしょに書けるテンプレートエンジン」みたいな活用ができる。
既存のライブラリでそういう活用をしているものがいくつかあるので、この記事ではそのアプローチを整理する。
React 以外で jsx, tsx を扱えるようにするアプローチ
jsx 関連のコンパイルオプションを変更する
jsx, tsx はコンパイラ (tsc, babel など) によって js, ts ファイルに変換されている。通常だと React 用の jsx ランタイムコードを呼び出すように変換している。
イメージとしてはこんな感じの変換が行われる。
export const HelloWorld = () => <h1>Hello world</h1>;
import React from 'react';
export const HelloWorld = () => React.createElement("h1", null, "Hello world");
(Ref: https://www.typescriptlang.org/tsconfig/#jsx)
ただ、変換方法はコンパイラの設定によってカスタマイズでき、そのライブラリのランタイムコードに変わりに変換することができる。
具体的なライブラリとしては、 preact (小さいサイズの React 五感実装), @emotion/react (React component に css props を追加する), @vscode/prompt-tsx (HTML ではなく、LLM 用のプロンプトを出力できるようにする) などが使っている。
tsc だと jsxFactory, jsxFragmentFactory, jsxImportSource などの設定でカスタマイズが行える。
このような設定を tsconfig.json に書くことで設定できる。
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
//...
}
}
ちなみに、これらの設定はマジックコメントでファイル単位で変更する、ということもできる。
こんな感じで jsx のランタイムコードを差し替えることができる。
/** @jsxImportSource . */
const sample = <SampleComponent title={"Custom Title"} />;
export function jsx(type: string | Function, props: any[], key: any, _source: any, _self: any) {
console.log("JSX Type:", type);
console.log("JSX Props:", props);
return { type, props, key, _source, _self };
}
$ ts-node sample.tsx
JSX Type: [Function: SampleComponent]
JSX Props: { title: 'Custom Title' }
この方法だと何でもできるが、利用する場合に jsx 関係のコンパイラオプションを変更して貰う必要があり、そこが若干面倒。
独自の renderer を定義する
React コンポーネントを実際に DOM 上に表示する場合、通常の React だと例えばこう書く。 (最近はフレームワーク使うので直接書かないかもだが…)
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
この render は ReactNode を受け取って実際の DOM 上での表示を行ってくれるのだが、これを独自の実装に置き換えることで、独自に使うことができる。
Ink ではそういうアプローチを取っている。
Ink はいわゆる TUI を実現するライブラリで、最近だと Claude Code や Gemini CLI などで使われている。React のターミナル版として動く。
こんな感じで、 React の jsx ランタイムコードを使いつつ、 render は Ink 独自のものを使う。
import React, {useState, useEffect} from 'react';
import {render, Text} from 'ink';
const Counter = () => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCounter(previousCounter => previousCounter + 1);
}, 100);
return () => {
clearInterval(timer);
};
}, []);
return <Text color="green">{counter} tests passed</Text>;
};
render(<Counter />);
(Ref: https://github.com/vadimdemedes/ink)
(おまけ) react-reconciler を使って独自の出力向けの宣言的 UI を作る
独自に renderer を実装するにしても、いわゆる宣言的 UI を実装するには React の Component 再計算の仕組みとかを実装する必要がある。
実は、 React から react-reconciler にはという仮想 DOM の再計算などのロジックを切り出したライブラリが提供されている。これを使うことで独自の宣言的 UI が実現できる。
使い方は README.md にも提供されていたり、React ART, React Native 向けの実装がサンプルとして提供されている。
Ink でもこれを使っている (source)
詳しくはこれらを参考にしながら作ってみるとよい。 (※ API は stable ではないとあるので、使用する際には、そのへんは注意)