はじめに
React 開発でおなじみの JSX は強力な仕組みですが、JSX の要素は「式 (Expression)」として評価されるため、コンポーネントの構造によっては制御構文(if や for)の扱いに工夫が必要になる場面もあります。
今回は、そうした JSX のパラダイムを拡張し、JavaScript/TypeScript の標準的な制御構文をそのまま使って直感的にコンポーネントを記述できるようにする新しい構文/コンパイラ TSRX を紹介します。
TSRX とは?
TSRX は、React、Preact、Solid、Vue などの様々な UI フレームワーク向けのコードへコンパイルできるツールです。
なお、2025年5月時点ではアルファ段階のプロジェクトのため、もし本番利用をする場合は注意してください。
最大の特徴は Statement-based JSX という設計を採用している点です。
従来の JSX が「式」であったのに対し、TSRX では JSX 要素を「文 (Statement)」として扱います。
これにより、JSX の枠組みに縛られすぎることなく、見慣れた制御構文をそのままコンポーネントの記述へと組み込めるようになっています。
基本構文
TSRX では、コンポーネントの書き方がいくつかの点で JSX (TSX) から大きく変わります。
公式ドキュメントの例を交えながら、既存の JSX との違いを見ていきましょう。
component キーワード
TSRX では function の代わりに component キーワードを使います。
JSX が「文」として扱われるため、関数の最外殻で return を書く必要がありません。
また、静的なテキストは "" で明示的に囲むルールになっています。
// Ref: https://tsrx.dev/features#jsx
component Greeting() {
<h1>"Hello World"</h1>
<p>"Welcome to TSRX."</p>
}
if / switch による条件分岐
JSX 内に条件分岐を書く際は、三項演算子(? :)や論理積(&&)を用いるのが一般的ですが、TSRX では if や switch がコンポーネントの中でそのまま機能します。
// Ref: https://tsrx.dev/features#if
component StatusBadge({ status }: { status: string }) {
<div>
if (status === 'active') {
<span class="badge active">"Online"</span>
} else if (status === 'idle') {
<span class="badge idle">"Away"</span>
} else {
<span class="badge">"Offline"</span>
}
</div>
}
// Ref: https://tsrx.dev/features#switch
component StatusMessage({ status }: { status: string }) {
switch (status) {
case 'loading':
<p>"Loading..."</p>
break;
case 'success':
<p class="success">"Done!"</p>
break;
default:
<p>"Unknown status."</p>
}
}
for...of によるリスト描画
.map() を使ったリスト描画の代わりに、for...of 文が使えます。
TSRX 独自の拡張として、ループのインデックスや React の key 属性に相当する指定も構文レベルでサポートされています。
// Ref: https://tsrx.dev/features#for
component TodoList({ items }: { items: Todo[] }) {
<ul>
for (const item of items; index i; key item.id) {
if (item.hidden) continue;
<li>{i + 1}". "{item.text}</li>
}
</ul>
}
for, for...in, while, do...while といった、for...of 以外のループ制御文は、描画のために使うことができません。
また、for...of 内であっても、return や break は使用できません(continue は使用可能です)。
要素スコープ内での変数宣言
従来の React では、JSX の途中で変数を宣言したい場合、関数を作って実行するなど少々冗長な書き方をする必要がありました。
TSRX では、それぞれの要素の内部が独立したスコープになります。
そのため、わざわざ関数で囲むことなく、必要な場所で直接変数を宣言できます。
// Ref: https://tsrx.dev/features#scoping
component App() {
const name = 'World';
<div>
// ネスト内部で自由に変数宣言が可能
const greeting = `Hello, ${name}!`;
<h1>{greeting}</h1>
</div>
}
React の定番パターンとの比較
ここからは、コンポーネントのスタイリング手法や、エラーハンドリング・Hooks といった React の機能が、TSRX のアプローチでどう変わるのかを紹介します。
組み込みの Scoped styles
React でのスタイリングは、Tailwind CSS のようなユーティリティファーストな手法や、CSS Modules、CSS-in-JS(Emotion など)を導入するのが一般的です。
一方、TSRX には Vue や Svelte のような Scoped styles が機能として組み込まれています。
// Ref: https://tsrx.dev/features#scoped-styles
component Card() {
<div class="card">
<h2>"Scoped title"</h2>
</div>
<style>
.card { padding: 1.5rem; border: 1px solid #ddd; }
h2 { color: #333; }
</style>
}
コンパイラがセレクタに一意のハッシュを自動付与するため、スタイルが他へ漏れ出すことはありません。
また、{style "className"} という構文を使って、スコープ化されたクラス名を子コンポーネントへ安全に渡す仕組み(Style composition)も用意されています。
// Ref: https://tsrx.dev/features#style-composition
component Badge({ className }: { className?: string }) {
<span class={`badge ${className ?? ''}`}>
"New"
</span>
<style>
.badge { padding: 0.25rem 0.5rem; }
</style>
}
component App() {
<Badge className={style 'highlight'} />
<style>
.highlight { background: #e8f5e9; color: #2e7d32; }
</style>
}
try / pending / catch による非同期・例外制御
React で非同期コンポーネントのローディング(Suspense)やエラーハンドリング(ErrorBoundary)を実装する場合、専用のコンポーネントでラップする必要があります。
TSRX では、これを try / catch の拡張構文で表現できます。
// Ref: https://tsrx.dev/features#async-boundaries
const UserProfile = lazy(() => import('./UserProfile.tsrx'));
export component App() {
try {
<UserProfile id={1} />
} pending {
<p>"Loading..."</p>
} catch (e) {
<p>"Something went wrong."</p>
}
}
これがコンパイラによってどのように変換されるかを見ると、TSRX の役割がよく分かると思うので、TSRX Playground でのコンパイル結果を見てみましょう。
import { Suspense } from 'react';
import { TsrxErrorBoundary } from '@tsrx/react/error-boundary';
const UserProfile = lazy(() => import('./UserProfile.tsrx'));
const App__static1 = <UserProfile id={1} />;
const App__static2 = <p>{'Loading...'}</p>;
const App__static3 = <p>{'Something went wrong.'}</p>;
export function App() {
return (
<TsrxErrorBoundary
fallback={(e, _reset) => {
return App__static3;
}}
>
<Suspense
fallback={(() => {
return App__static2;
})()}
>
{(() => {
return App__static1;
})()}
</Suspense>
</TsrxErrorBoundary>
);
}
このように、TSRX は標準的な <Suspense> と <ErrorBoundary> の構造へ静的に変換することで、シンプルな文法を実現しているということが分かります。
Hooks ルールのコンパイラ対応
React には、「フックはトップレベルでのみ呼び出す」という重要なルールがあります。
つまり、ループや条件分岐、早期リターンの後などでフックを呼ぶことはできません。
しかし TSRX では、以下のように本来ルールに反する位置での hooks の記述が許可されています。
// Ref: https://tsrx.dev/features#react-hooks
import { useState, useEffect } from 'react';
component UserProfile({ user, posts }: { user: User | null; posts: Post[] }) {
if (!user) {
<p>"Please sign in."</p>
return;
}
// 早期リターンの後ろにある Hook
const [tab, setTab] = useState('overview');
<h1>{user.name}</h1>
<TabBar value={tab} onChange={setTab} />
<ul>
for (const post of posts) {
// ループの中にある Hook
useEffect(() => {
console.log(`viewed ${post.title}`);
}, [post.title]);
<li>{post.title}</li>
}
</ul>
}
これはルールを実際に無視しているわけではありません。
TSRX のコンパイラは、条件分岐やループの中、あるいは早期リターンの後ろにある hooks を検知すると、そのブロックを独立した別の子コンポーネントとして自動的に分離生成します。
ランタイム上では、生成された子コンポーネントのトップレベルでフックが呼ばれている状態へと正しくマッピングされるため、React のルールを遵守しつつ、記述上の制約を回避できるようになっています。
コンパイラが生成した子コンポーネントの境界を越えて、フックの戻り値を代入したり、親スコープのバインディングを変更しようとしたりした場合はコンパイルエラーになります。
フックの結果を複数のスコープで共有する必要がある場合は、明示的な子コンポーネントへ状態を切り出すといったような設計が必要です。
まとめ
TSRX は、JSX を「文 (Statement)」として捉え直すことで、より JavaScript/TypeScript の自然な制御構文を活用できるようにするユニークなコンパイラです。
従来の JSX では工夫が必要だった条件分岐やリスト描画も、直感的な構文で書けるようになります。
また、コンパイラがフックのルールなどをコードレベルで解決してくれるため、React の作法を守りつつ、より自由度の高い実装が可能になるのも大きな魅力です。
詳細が気になる方はぜひ TSRX の公式ドキュメントや Playground を触ってみてください。