Standard JSX works. Components run once — there is no re-render — so read signals inside JSX (count()) and the compiler wires each read to a patchable slot.
#Control flow
// Ternary
{count() > 0 ? <p>{count()} items</p> : <p>No items</p>}
// Logical AND
{isLoggedIn() && <Dashboard />}#List rendering
Every .map() row needs a key. .filter(), .sort(), and .toSorted() chain before it; a block-body predicate or comparator works when the body produces a value (see Adapter limits).
{todos().map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}// Filter with a block-body predicate
{todos().filter(t => {
const f = filter()
if (f === 'active') return !t.done
if (f === 'completed') return t.done
return true
}).map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
// Sort by price, break ties by name
{items().sort((a, b) => a.price - b.price || a.name.localeCompare(b.name)).map(item => (
<Item key={item.id} item={item} />
))}#Event handling
<button onClick={() => setCount(n => n + 1)}>+1</button>
<input onInput={(e) => setText((e.target as HTMLInputElement).value)} />
<input onKeyDown={(e) => e.key === 'Enter' && handleSubmit()} />#Dynamic attributes
<button disabled={!accepted()}>Submit</button>
<a className={filter() === 'all' ? 'selected' : ''}>All</a>
<div style={`background: ${accepted() ? '#4caf50' : '#ccc'}`}>Status</div>#Fragments
<>…</> works, and a component may return one. No wrapper element is emitted — the children land directly in the parent, and <>{props.children}</> is passed through as if the fragment were not there. In a "use client" component with several returns, a branch that returns a bare fragment is refused with BF029; wrap that branch in an element.
<>
<h1>Title</h1>
<p>Description</p>
</>#Adapter limits
JS-runtime adapters (Hono, CSR) execute any callback at SSR, so everything above compiles there. Template-language adapters (Go, Mojolicious, Xslate, ERB, Jinja, Twig, Blade, minijinja, Pebble) lower a defined subset into their template grammar and refuse the rest loudly:
- BF021 — an unsupported predicate or comparator shape: a genuinely imperative block body (one that reassigns a local, loops, or
breaks — a value-producing block with pureconstbindings normalizes and lowers everywhere),typeof, a comparator referenced from an import or through an alias. - BF101 — an expression with no lowering at all:
.reduce(),.forEach(), a nested.some()/.find()inside a predicate, or a loop over a component-scopeconstcomputed at render time.
Both are escapable with /* @client */, which renders nothing for that region until hydration. When the value can be computed server-side, pass it as a prop instead — that keeps SSR.
// ❌ BF101 on Go/Mojo; works on Hono
type Props = { reactions: Record<string, string[]> }
function ReactionBar(props: Props) {
const entries = Object.entries(props.reactions).filter(([, users]) => users.length > 0)
return <div>{entries.map(([emoji, users]) => (
<span key={emoji}>{emoji}: {String(users.length)}</span>
))}</div>
}
// ✅ Pass the array as a prop — keeps SSR
type Entry = [string, string[]]
function ReactionBarByProp({ entries }: { entries: Entry[] }) {
return <div>{entries.map(([emoji, users]) => (
<span key={emoji}>{emoji}: {String(users.length)}</span>
))}</div>
}// ❌ BF101 on Go/Mojo; works on Hono
{items().filter(x => x.tags.some(t => t.active)).map(t => t.name)}
// ✅ Defer to the client — empty until hydration
{/* @client */ items().filter(x => x.tags.some(t => t.active)).map(t => t.name)}Per-adapter table: compatibility matrix. Diagnostics and fixes: Error Codes.