Templating

JSX is standard — use ordinary JavaScript for control flow. The compiler turns dynamic expressions into surgical DOM updates anchored by hidden comment nodes.

Conditionals

Ternaries and && work inline in JSX and stay reactive when they can produce DOM nodes:

JSX
{isLoading ? <Spinner /> : <Content />}
{error && <p class="error">{error}</p>}

The compiler wraps these inline holes in a dynamic region: a comment anchor plus a bindChild effect that swaps the rendered branch when the condition changes — without rebuilding the parent.

Init-once vs reactive conditionals

A component body runs once when the element first connects (connectedCallback). Local variables, if statements, and JSX assigned to const/let are evaluated in that single pass. Only regions the compiler wires to signals — inline JSX holes, text holes, lists, props — keep updating afterward.

Think of it as two layers:

LayerWhen it runsWhat updates later
Component bodyOnce at first connectNothing — unless you read signals inside an $effect
Inline JSX holesBuilt once, then managed by effects{cond ? <A/> : <B/>}, {text}, {items.map(…)}

Reactive — use these for show/hide and layout switches

Put the conditional inside JSX (or return a conditional expression whose branches are JSX). Signal reads in the expression subscribe the region:

Root conditional with JSX branches:

JSX
export default function Dashboard() {
  let user = $state(null);
  return user ? <MainApp user={user} /> : <Login />;
}

Stable outer shell, reactive inner hole:

JSX
export default function Dashboard() {
  let user = $state(null);
  return (
    <div class="shell">
      {user ? <MainApp user={user} /> : <Login />}
    </div>
  );
}

&& short-circuit holes behave the same way — a falsy condition removes the branch; truthy mounts it again on the next change:

JSX
{isAdmin && <AdminPanel />}

Init-once — frozen at first connect

These patterns read signals once during initialization. Changing the signal later does not swap the UI.

Early return guard — frozen at first connect:

JSX
export default function Dashboard() {
  let user = $state(null);
  if (!user) return <Login />; // ❌ never flips when user logs in
  return <MainApp user={user} />;
}

JSX cached in a local — builders run once, branch is fixed:

JSX
export default function Dashboard() {
  let user = $state(null);
  const panel = user ? <MainApp user={user} /> : <Login />; // ❌ evaluated once
  return <div>{panel}</div>; // `panel` is a plain value, not a reactive hole
}

Separate top-level return paths — the compiler builds the view from a single return expression; guard returns in if blocks do not become a reactive switch:

JSX
export default function Dashboard() {
  let user = $state(null);
  if (user) {
    return <MainApp user={user} />; // ❌ not a reactive region
  }
  return <Login />;
}
Rule of thumb

If the conditional must react to $state, write it as an inline JSX hole or as a single return whose branches are JSX (return cond ? <A/> : <B/>). Do not guard with if (…) return … or cache JSX in a variable.

Stable root for layout chrome

When a shell (header, nav) should stay put while an inner region swaps, keep the outer nodes static and put the conditional on the inner hole: return <div class="shell">{page ? <Page /> : <Login />}</div>.

Lists

Render arrays with .map. Give each item a stable key so updates reorder nodes instead of rebuilding them:

JSX
<ul>
  {todos.map((todo) => (
    <li key={todo.id}>{todo.text}</li>
  ))}
</ul>
Keys enable minimal moves

With keys, reordering a list moves the existing DOM nodes (longest-increasing-subsequence reconciliation) rather than re-creating them.

Refs

$ref() gives you a handle to a DOM node. Read it after mount:

JSX
export default function Field() {
  const input = $ref();

  onMount(() => input.focus());

  return <input ref={input} />;
}

A $ref is a reactive signal whose value is the element (or null before it mounts). Reading it inside $effect subscribes to it, so the effect re-runs whenever the node changes — e.g. when a conditional swaps <input> for <textarea>:

JSX
const el = $ref();
$effect(() => { if (el) measure(el); }); // re-measures when the node changes

No callback refs — use $ref + lifecycle instead

ref takes a $ref, not a function. A function-valued ref (ref={(el) => …}, React's "callback ref") is rejected by the compiler, because everything a callback ref does is expressed more directly with $ref plus effects, lifecycle, and $expose:

You want to…Do this
Access / focus / scroll a node$ref + onMount (or an event handler)
Measure a node, re-measure when it changes$ref read inside $effect
Initialize a library on a node, tear it downonMount(() => { const x = init(el); return () => x.destroy(); })
Clean up when the element leavesonCleanup(() => …)
Give a parent an imperative handle$expose — expose an API, not the raw node
Manage a dynamic set of elements (focus/scroll item N)Make each item its own component that owns its $ref and $exposes what the parent needs
JSX
// Initialize + tear down a third-party editor on the element:
export default function Editor() {
  const el = $ref();
  onMount(() => {
    const cm = new CodeMirror(el);
    return () => cm.destroy();
  });
  return <div ref={el} />;
}
Why not callback refs?

A $ref is reactive and the framework has explicit lifecycle hooks, so a callback ref adds no capability — only a second way to receive a node. Exposing an API from a child (via $expose) is also a stronger boundary than handing out its DOM. See the full Imperative API guide.