Skip to content

JSX & React

Floe supports JSX natively. Write React components with Floe’s type system.

import { useState } from "react"
export let Counter() -> JSX.Element = {
let (count, setCount) = useState(0)
<div>
<h1>Count: {count}</h1>
<button onClick={() -> setCount(count + 1)}>Increment</button>
</div>
}

Components are exported let declarations with a JSX.Element return type. The last expression is the return value.

type ButtonProps = {
label: string,
onClick: () -> (),
disabled: boolean,
}
export let Button(props: ButtonProps) -> JSX.Element = {
<button
onClick={props.onClick}
disabled={props.disabled}
>
{props.label}
</button>
}

Use match expressions:

<div>
{match isLoggedIn {
true -> <UserProfile user={user} />,
false -> <LoginForm />,
}}
</div>

For the common “render if present, nothing if absent” pattern, use Option.map. Since None compiles to undefined and React ignores undefined children, Option<JSX.Element> works directly in JSX:

// Instead of matching with a None -> <></> arm:
<div>
{user.nickname |> Option.map((nick) -> <span className="badge">{nick}</span>)}
</div>

This replaces the TypeScript {x && <Component />} pattern, without the truthiness pitfalls (e.g. {count && <Tag />} rendering 0).

Use pipes with map:

<ul>
{items |> map((item) -> <li key={item.id}>{item.name}</li>)}
</ul>

Forward all props to a child element:

export let Card(props: CardProps) -> JSX.Element = {
<div {...props} className="card" />
}

Compiles to <div {...props} className={"card"} /> in TypeScript.

Compound component patterns using dot notation are supported:

import { Select, ListBox } from "ui"
export let Picker() -> JSX.Element = {
<Select>
<Select.Trigger>Choose...</Select.Trigger>
<Select.Value />
<Select.Popover>
<ListBox>
<ListBox.Item key="a">Alpha</ListBox.Item>
<ListBox.Item key="b">Beta</ListBox.Item>
</ListBox>
</Select.Popover>
</Select>
}

This is common in component libraries like React Aria, Radix UI, and similar. The tag name compiles as-is to TypeScript.

<>
<Header />
<Main />
<Footer />
</>

The compiler automatically emits .tsx when JSX is detected, and .ts otherwise. No configuration needed.

React 19 removed the global JSX namespace and moved it under React.JSX. You still write JSX.Element in Floe. The compiler adds the import the emitted file needs:

import type { JSX } from "react";
export function Counter(): JSX.Element {
// ...
}

The import is type-only, so every bundler erases it and the emitted module gains no runtime dependency on React. Import the namespace yourself, as import { JSX } from "react", and the compiler leaves your import alone.

What’s Different from React + TypeScript

Section titled “What’s Different from React + TypeScript”
  • No class components - only function components
  • No any in props - every prop must be typed
  • Pipes instead of method chaining for data transformations
  • Pattern matching instead of ternaries for complex conditionals