TSX is TypeScript with JSX syntax. It lets you write markup such as
<h1>Hello</h1> inside TypeScript. Forma's build turns that markup into calls to
Forma's h() function. You can also write those calls yourself in a .ts file.
Both styles use the same rendering model.
You do not need to run a separate JSX → TypeScript → JavaScript command chain. The browser build handles types and JSX together; a native build also compiles the supported page structure into FMIR for Rust.
Choose TSX or h()#
These components describe the same heading. With TSX, save the source as .tsx:
import { h } from "@getforma/core";
export function Heading(props: { title: string }) {
return <h1 class="page-title">{props.title}</h1>;
}With explicit h() calls, it can be a .ts file:
import { h } from "@getforma/core";
export function Heading(props: { title: string }) {
return h("h1", { class: "page-title" }, props.title);
}Choose the form you find easier to read. JSX does not require React: Forma's
configured JSX factory is h, with Fragment for fragments. The TypeScript
jsx: "react" setting in the starter names the classic JSX transform; its
jsxFactory: "h" setting selects Forma's function.
Reactivity is the same in either syntax. Components run once, so a changing value
needs an accessor, such as {() => count()} in TSX or () => count() as an
h() child. An event handler such as onClick remains a function that handles
the event.
Follow the browser build#
@getforma/build uses esbuild to remove TypeScript types, transform JSX and
bundle the browser code. Those operations are integrated into the build; there
is no intermediate .ts file you need to create or manage.
Browser UI:
.tsx or .ts → esbuild → browser JavaScript using Forma h()
Native page, when SSR is enabled:
.tsx → internal JSX-to-h() transform ─┐
.ts with h() ────────────────────────┴→ supported tree → FMIR → Rust → HTML
original TypeScript declarations ─────→ declared props schemaThe browser receives JavaScript, so it does not parse JSX or TypeScript at
runtime. A browser-only application using @getforma/core needs no FMIR output
or Rust server. You still use a bundler for TypeScript source, including plain
h() calls in a .ts file.
The published browser build options
set jsx: "transform", jsxFactory: "h" and jsxFragment: "Fragment".
Follow the native build#
For native SSR, the compiler internally lowers .tsx/.jsx markup to h()
calls, then analyzes the supported component tree and emits FMIR. Rust loads
that representation and renders HTML with the request's typed props. The Rust
renderer does not run JSX, React or a JavaScript engine.
The compiler also reads the original TypeScript declarations to build the props schema before JSX lowering removes their type annotations. Types are removed from browser JavaScript, but the declared page contract is retained in the native output. See the declared props guide.
Native rendering supports a defined subset of expressions. Valid TypeScript
and valid JSX are not enough to guarantee native compilation. Compute business
logic or unsupported derived values in Rust, then pass explicit presentation
props to the page. Writing h() by hand does not expand that subset. See the
native subset for supported forms and diagnostics.
The implementation is visible in the current module loader and declared schema reader.
Use the same build commands#
TSX does not add commands to the installation steps.
In the published minimal starter, run npm run build:ssr from admin/ to
produce FMIR and its paired browser assets, then run Cargo from the application
root. The starter's ordinary npm run build selects browser rendering instead.
Choosing TSX adds syntax transformation work during the build. This guide does
not establish a measured build-time difference between TSX and handwritten
h(), and does not claim that the transform is free. Both TypeScript forms
still need the build; neither adds a JSX parser to the browser or Rust renderer.
Keep native modules, browser assets and the manifest from the same build.
Check types separately#
Transpilation turns source into runnable output. Type checking checks whether your TypeScript uses types consistently. Native compilation checks whether the page can be represented in FMIR. These are distinct checks.
For a project with TypeScript installed and an appropriate tsconfig.json, run
the type checker from that frontend project's directory:
npx tsc --noEmitAdd this to your own validation scripts if you want it as a required check.
The published minimal starter's build commands do not implicitly run this
type-checking step or provide a typecheck script. Passing it also does not
replace the native build's subset checks or Rust's props binding.
Keep routing and activation separate#
The application server owns HTTP routing. In the minimal starter, Axum matches a request to the application's handler; the handler supplies typed props and returns Forma's complete page response. The build's route configuration pairs that route with its page and assets. Follow application configuration for that wiring.
On a rendered page, island activation attaches interaction to declared islands and adopts their existing DOM. It does not implement browser navigation between pages. Ordinary links can request another document from the server, including when JavaScript is disabled.
The optional @getforma/router package adds navigation between compatible pages.
This website uses its generated-HTML adapter for portal pages and its native
adapter for the router lab. Ordinary native pages retain document
navigation. Use one coordinator per document and preserve each island's owner.
The routing guide explains the three adapters and their boundaries.