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


[TSRX](https://tsrx.dev/) (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"](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/93ea3b3e-a219-4419-7399-50eb1e012000/orig =1920x1080)

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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/cd7abf05-7084-4585-2754-1e2c3b2ddd00/lg2x =1920x1080)


## Setup with Vite

<iframe width="100%" height="315" src="https://www.youtube.com/embed/B_N-O8_eG34" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


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

```command
npm create vite@latest my-tsrx-app -- --template react-ts
```

```command
cd my-tsrx-app
```

```command
npm install
```

```command
npm install @tsrx/vite-plugin-react --save-dev
```

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

```typescript
[label 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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/1c480f65-0892-4d57-a4d0-31e0ad6f4500/lg1x =1920x1080)

## 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:

```typescript
[label 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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/e1a39f89-3585-4d15-bdec-a254727d8900/orig =1920x1080)

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):

```typescript
[label 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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/80a31a47-08c7-46e6-3efd-24b9f153eb00/lg1x =1920x1080)

`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:

```typescript
[label 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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/60b260fa-360f-4e71-6f31-5b2fa0edd400/lg2x =1920x1080)

- `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:

```typescript
[label 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:

```typescript
[label 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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/5a64f522-1531-44ab-b20b-924666ef4700/md1x =1920x1080)

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

```typescript
[label 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](https://tsrx.dev/).