Two syntaxes, one Octane
TSRX vs TSX/JSX
Use TSX when familiarity matters. Use TSRX when its template syntax makes the UI easier to read. Both use the same components, APIs, and runtime.
Which should I use?
Choose TSX or JSX when you are moving an existing React component, following a
React example, or sharing source that must remain valid React code. It is the syntax
you already know, compiled for Octane. React examples are not always a literal paste:
text inputs use native onInput for every edit, for example. In both dialects,
OCTANE_NATIVE_TEXT_ONCHANGE warns when a text host appears to carry React-style
onChange; native commit-on-blur can be marked with the non-serialized
suppressNativeChangeWarning host hint. Check
Differences from React when porting a component.
Choose TSRX for new Octane code when lists and branches are a large part of the output. It keeps rendered control flow in the template and gives the compiler an explicit path to optimize it.
The practical default is simple: do not rewrite working TSX just to change the file extension. Start new components in TSRX, then choose whichever style makes each file easier to understand.
The same component, with less ceremony
TSRX adds @{ ... }, a shorthand for a function body that returns one JSX element or
fragment. Setup stays at the top and the final node is the output:
export function Counter() @{
const [count, setCount] = useState(0);
<button onClick={() => setCount(count + 1)}>{'Count: ' + count}</button>
}The TSX version uses an explicit return:
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{'Count: ' + count}</button>;
}These compile to the same kind of component. @{ ... } is optional in a .tsrx
file, so use a normal function body when an early return or a non-JSX value reads
better.
Branches and lists that read top to bottom
TSX puts rendered control flow inside JavaScript expressions: ternaries, &&, and
.map(). TSRX can express the same decisions as template blocks:
export function SearchResults(props) @{
@if (props.loading) {
<p>Searching…</p>
} @else {
<ul>
@for (const result of props.results; key result.id) {
<li>{result.name as string}</li>
} @empty {
<li>No matches</li>
}
</ul>
}
}Directive arms keep JavaScript setup and template output separate, just like an
@{ ... } component body. An ordinary expression statement is setup and does not
render, even when it is the arm's only statement. This is deliberate:
@if (ready) { console.log(value); } logs without treating the call as UI. The same
rule means a bare renderable value is evaluated but not output:
@if (ready) {
value; // setup: evaluated, but not rendered
};Make a computed output explicit by wrapping its expression hole in a fragment:
@if (ready) {
<>
{value}
</>
};The same rule applies to @else, @for/@empty, @switch cases and defaults,
and @try/@pending/@catch.
The key tells Octane which item is which when the list changes. The @empty branch
keeps the empty state beside the list it belongs to. Simple keyed .map() expressions
still work when that reads better.
TSRX also provides @switch for several branches and @try with @pending and
@catch for async and error states. Plain JavaScript control flow still belongs in
the setup part of the function; directives are for the UI it returns.
Text holes make the output explicit
A value between tags can be text or another renderable value. TSRX asks you to make that distinction clear when the compiler cannot prove the answer:
<h2>{product.name as string}</h2>
<p>{'In stock: ' + product.count}</p>
<Card>{product.details}</Card>Use as string for dynamic text whose type is not obvious to the compiler. A string
literal, template literal, or string concatenation needs no cast. Leave a renderable
value bare when it may be a component, element descriptor, array, primitive, or
null.
The cast is a compile-time signal, not a runtime conversion. Standard TSX does not
make this distinction; {expression} accepts any renderable value, as it does in
React.
One rule also applies to both: do not call a slot-based hook directly in a plain
JavaScript loop. Use keyed @for, or move the hook into a row component rendered by a
keyed .map().
Next
- Quick start — install, mount, and the
.tsrxessentials. - Build tools — Vite, Rspack, and Rsbuild integration.
- Core APIs — roots, hooks, boundaries, actions, and SSR.
- Differences from React — the deliberate divergences (everything else matching React is the point).