Back to Scaling Node.js applications guides

TSRX: Statement-Based Component Syntax as a Successor to JSX

Stanley Ulili
Updated on May 10, 2026

TSRX (TypeScript Render Extensions) is a TypeScript superset for writing UI components. It was created by Dominic Gannaway, creator of Ripple.js, and extracted from that framework as a standalone, framework-agnostic tool. It compiles to React, Preact, Solid, and Vue.

Dominic Gannaway's tweet announcing TSRX as the "spiritual successor to JSX"

The core distinction from JSX is that TSRX is statement-based rather than expression-based. Components do not require a return statement, and control flow uses native JavaScript statements (if/else, for...of, switch, try/catch) rather than expressions (ternaries, .map(), logical &&).

Official TSRX documentation definition highlighting its focus on readability and co-location

Setup with Vite

TSRX requires a compilation step. Component files use the .tsrx extension. Setup for a React project:

 
npm create vite@latest my-tsrx-app -- --template react-ts
 
cd my-tsrx-app
 
npm install
 
npm install @tsrx/vite-plugin-react --save-dev

Add the plugin to vite.config.ts before the React plugin:

vite.config.ts
import { defineConfig } from 'vite'
import tsrxReact from '@tsrx/vite-plugin-react'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [
    tsrxReact(),
    react()
  ],
})

The vite.config.ts file showing the addition of the tsrxReact() plugin

Core syntax differences

The component keyword and absent return

A TSRX component uses the component keyword instead of function, and the markup is written directly in the component body without a return statement:

Greeting.tsrx
export component Greeting({ name }: { name?: string }) {
  <div class="card">
    if (name) {
      <p>"Hello, "{name}</p>
    } else {
      <p>"Hello, stranger"</p>
    }
  </div>
}

Introductory TSRX component code showing the component keyword and if/else statement

The component body renders top-to-bottom. Static text strings are wrapped in double quotes. Class attributes use class rather than className.

return still works for early exits (guard clauses):

UserProfile.tsrx
export component UserProfile({ user }) {
  if (!user) {
    <p>"Please sign in."</p>
    return;
  }

  <div>
    <h2>{user.name}</h2>
  </div>
}

Conditional rendering with if/else and switch

JSX requires ternary operators for conditional rendering. TSRX uses standard if/else:

React TSX: typescript function Status({ state }: { state: 'online' | 'away' | 'offline' }) { return ( <div className="card"> {state === 'online' ? ( <span className="ok">Online</span> ) : state === 'away' ? ( <span className="warn">Away</span> ) : ( <span className="off">Offline</span> )} </div> ); }

TSRX: typescript [label Status.tsrx] component Status({ state }: { state: 'online' | 'away' | 'offline' }) { <div class="card"> if (state === "online") { <span class="ok">"Online"</span> } else if (state === "away") { <span class="warn">"Away"</span> } else { <span class="off">"Offline"</span> } </div> }

Side-by-side comparison showing the cleaner if/else structure of TSRX versus the nested ternary in React

switch statements can also be used directly without wrapping them in an immediately invoked function expression.

List rendering with for...of

Instead of .map(), TSRX uses a for...of loop with extended syntax:

TodoList.tsrx
component TodoList({ items }) {
  <ul>
    for (const todo of items; index i; key todo.id) {
      if (todo.archived) continue;

      <li>{i + 1}. {todo.text}</li>
    }
  </ul>
}

TSRX for...of loop highlighting the index and key syntax

  • index i provides the loop iteration index as i
  • key todo.id is the stable identifier equivalent to React's key prop
  • continue skips rendering an item, providing inline filtering without a separate .filter() call

Async boundaries and error handling

TSRX provides try/catch/pending blocks for async and error handling:

Profile.tsrx
component Profile({ id }) {
  <div class="card">
    try {
      <UserCard id={id} />
    } pending {
      <div class="loading">"Loading..."</div>
    } catch (err) {
      <div class="err">"Couldn't load user"</div>
    }
  </div>
}

The compiler transforms pending into the appropriate primitive for the target framework. For React, it generates <Suspense> and an error boundary. In standard React, both of these require separate, verbose implementations.

Hook calls inside conditions and loops

React's Rules of Hooks prohibit calling hooks inside conditions or loops. TSRX removes this restriction from the developer's perspective:

UserProfile.tsrx
import { useState, useEffect } from 'react';

component UserProfile({ user, posts }) {
  if (!user) {
    return;
  }

  const [tab, setTab] = useState('overview');

  <ul>
    for (const post of posts) {
      useEffect(() => {
        console.log('viewed ' + post.title);
      }, []);

      <li>{post.title}</li>
    }
  </ul>
}

TSRX component demonstrating useState and useEffect hooks within conditional blocks and loops

The TSRX compiler hoists all hook calls to the top of the generated function before passing it to React. React still receives hooks in a stable order, satisfying its requirements. The developer writes hooks co-located with the markup and logic they relate to.

Scoped styles

Card.tsrx
component Card() {
  <div class="card">
    <Notice />
  </div>

  <style>
    .card {
      padding: 1.5rem;
      background: #0b3af2;
      color: white;
    }
  </style>
}

component Notice() {
  <div class="card">"Heads up!"</div>
}

The .card styles in Card apply only to elements inside Card. They do not affect the div in Notice despite the shared class name. The compiler adds a unique attribute or hashed class name at build time to scope each component's styles.

Tradeoffs

TSRX introduces a new syntax and a required compilation step. Developers with strong JSX habits may find for...of loops and statement-based rendering unfamiliar at first. The technology is relatively new, which means smaller ecosystem, fewer examples, and less tooling support compared to JSX. AI code generation tools are heavily trained on JSX patterns, so generated suggestions may not match TSRX conventions.

The compiler-hoisted hooks feature is powerful but adds a layer of indirection between what the developer writes and what React receives. If generated output differs from expectations, debugging requires understanding the compilation step.

Final thoughts

TSRX is a well-considered attempt to make component syntax more consistent with the JavaScript developers already write. The if/else, for...of, and try/catch patterns are more readable to developers new to a codebase than nested ternaries and IIFE-wrapped switches. The hook hoisting is technically sound and removes a frequently cited pain point.

Whether the readability benefits outweigh the cost of adopting a new compilation step and syntax depends on the team. For projects already using single-file component frameworks like Svelte or Vue, TSRX's philosophy will feel familiar. For teams with heavy JSX investment, the migration cost is real.

Documentation and framework plugins are at tsrx.dev.

Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.