{"protocol":"clank-doc/1","frameworkVersion":"0.8.0","slug":"rendering","title":"Rendering and components","description":"Clank compiles TSX into direct DOM construction and fine grained reactive bindings. There is no virtual DOM and no whole component rerender loop. A component executes once to establish structure; expressions update independently afterward.","group":{"id":"framework","title":"Framework"},"url":"https://docs.clank.run/docs/rendering","source":"docs/rendering.md","headings":["TSX components","Automatic reactivity","Classes and styles","Events","Forms","Refs and directives","Lifecycle and context","Conditional control flow","Keyed lists","Lazy components and promises","Raw HTML and hydration","Compiler escape hatches"],"tableOfContents":[{"id":"tsx-components","title":"TSX components","level":2},{"id":"automatic-reactivity","title":"Automatic reactivity","level":2},{"id":"classes-and-styles","title":"Classes and styles","level":2},{"id":"events","title":"Events","level":2},{"id":"forms","title":"Forms","level":2},{"id":"refs-and-directives","title":"Refs and directives","level":2},{"id":"lifecycle-and-context","title":"Lifecycle and context","level":2},{"id":"conditional-control-flow","title":"Conditional control flow","level":2},{"id":"keyed-lists","title":"Keyed lists","level":2},{"id":"lazy-components-and-promises","title":"Lazy components and promises","level":2},{"id":"raw-html-and-hydration","title":"Raw HTML and hydration","level":2},{"id":"compiler-escape-hatches","title":"Compiler escape hatches","level":2}],"markdown":"# Rendering and components\n\nClank compiles TSX into direct DOM construction and fine-grained reactive bindings. There is no virtual DOM and no whole-component rerender loop. A component executes once to establish structure; expressions update independently afterward.\n\n## TSX components\n\n```tsx\ninterface BadgeProps {\n  tone: \"green\" | \"amber\";\n  children: Renderable | Renderable[];\n}\n\nfunction Badge(props: BadgeProps) {\n  return <span class={`badge badge-${props.tone}`}>{props.children}</span>;\n}\n\nfunction Status() {\n  return (\n    <section id=\"status\">\n      <Badge tone=\"green\">Ready</Badge>\n      <>One fragment can contain several siblings.</>\n    </section>\n  );\n}\n```\n\nLowercase tags create HTML/SVG elements. Uppercase or member-expression tags call components. Fragments create no wrapper element.\n\n`h(type, props, ...children)` remains available as a lower-level integration API, but application code normally uses TSX.\n\n## Automatic reactivity\n\nAny nonliteral expression in a TSX child or ordinary element property is compiled into a lazy reactive marker:\n\n```tsx\nconst count = signal(0);\nconst doubled = computed(() => count.value * 2);\n\nfunction Counter() {\n  return (\n    <button disabled={count.value >= 10} onClick={() => count.value++}>\n      Count {count.value}; doubled {doubled.value}\n    </button>\n  );\n}\n```\n\nThe compiler creates separate bindings for `disabled`, `count.value`, and `doubled.value`. The click handler remains a normal event callback. Updating `count` mutates the existing button property and text nodes without calling `Counter` again.\n\nComponent prop expressions are lazy too:\n\n```tsx\nfunction Greeting(props: { name: string }) {\n  return <h1>Hello {props.name}</h1>;\n}\n\n<Greeting name={profile.value.name} />\n```\n\nThe runtime exposes expression-backed props through getters. Reading `props.name` inside the heading's compiled expression observes the latest parent value.\n\nJavaScript executed imperatively in the component body still runs once. Use TSX expressions, `Show`, `Switch`, or an `effect` for behavior that must react.\n\n## Classes and styles\n\n```tsx\n<div\n  class={compact.value ? \"card compact\" : \"card\"}\n  classList={{\n    selected: selected.value,\n    disabled: !enabled.value,\n  }}\n  style={{\n    color: foreground.value,\n    \"--progress\": `${progress.value}%`,\n  }}\n/>\n```\n\nDynamic `class` updates the existing element. `classList` diffs active tokens, including space-separated token groups. Dynamic style objects remove missing properties and update current ones. Static Tailwind class strings pass through unchanged.\n\n## Events\n\n```tsx\n<button onClick={save}>Save</button>\n<button on:click={save}>Save</button>\n<button onClickCapture={save}>Capture</button>\n<button onClickOnce={save}>Run once</button>\n<button onTouchstartPassive={handleTouch}>Touch</button>\n```\n\nEvent expressions are not reactive wrappers. Listeners attach once and are removed on unmount. The `Capture`, `Once`, and `Passive` suffixes map to standard event-listener options.\n\n## Forms\n\n`bind:` creates a two-way connection to a signal:\n\n```tsx\nconst email = signal(\"\");\nconst accepted = signal(false);\n\n<input type=\"email\" bind:value={email} />\n<input type=\"checkbox\" bind:checked={accepted} />\n<p>{email.value}</p>\n```\n\n`bind:value` listens to `input`; other bound properties listen to `change`. The compiler passes the signal itself rather than wrapping it.\n\nFor validation, error state, cancellation, server errors, resets, and accessible field relationships, use `createForm()` rather than assembling those mechanics from individual signals. See [Forms](forms.md).\n\nClank's strict JSX declarations type native tags, element properties, event `currentTarget`, reactive values, ARIA/data attributes, bind/ref/use protocols, and hyphenated custom elements.\n\n## Refs and directives\n\n```tsx\nconst input = signal<HTMLInputElement | null>(null);\n\n<input\n  ref={input}\n  use={(element) => {\n    const observer = new ResizeObserver(report);\n    observer.observe(element);\n    return () => observer.disconnect();\n  }}\n/>\n```\n\n`ref` accepts a callback or signal. `use` accepts one directive or an array. A directive may return an unmount cleanup.\n\n## Lifecycle and context\n\n```tsx\nconst Theme = createContext(\"system\");\n\nfunction Shell() {\n  provideContext(Theme, \"dark\");\n  onMount(() => {\n    console.log(\"mounted\");\n    return () => console.log(\"unmounted\");\n  });\n  return <Page />;\n}\n\nfunction Page() {\n  const theme = useContext(Theme);\n  return <p>Theme: {theme}</p>;\n}\n```\n\nProviders affect descendant components mounted from that component's output. Contexts always have a default value. Component scopes own nested effects, resources, directives, event listeners, and cleanup callbacks.\n\n## Conditional control flow\n\n```tsx\n<Show when={user.value} fallback={<Login />}>\n  <Profile user={user.value} />\n</Show>\n\n<Switch fallback=\"Unknown\">\n  <Match when={status.value === \"ready\"}>Ready</Match>\n  <Match when={status.value === \"error\"}>Failed</Match>\n</Switch>\n```\n\nOnly the selected dynamic region is mounted. Changing the condition disposes the previous region before mounting the next.\n\n## Keyed lists\n\n```tsx\n<For each={tasks.value} by=\"id\" fallback={<p>No tasks</p>}>\n  {(task, index) => (\n    <article data-id={task.id}>\n      <span>{index() + 1}. {task.title}</span>\n      <button onClick={() => remove(task.id)}>Remove</button>\n    </article>\n  )}\n</For>\n```\n\n`For` calls the row function once per new key. Retained object rows receive a stable reactive proxy, so immutable replacements update property bindings without recreating the row. The second argument is an index accessor because reordering can change it.\n\nUse `by=\"id\"` for a record property or `by={(item) => item.id}` for a custom key. Duplicate keys throw instead of producing ambiguous DOM. See [Performance model](performance.md) for identity guarantees.\n\n## Lazy components and promises\n\n`lazy()` accepts a dynamic component import. A Promise can also be rendered directly; its pending marker is replaced when it resolves. Rejection renders the error message unless the surrounding application supplies its own error UI.\n\n## Raw HTML and hydration\n\n`dangerouslySetInnerHTML={{ __html }}` is deliberately explicit and bypasses child mounting. Sanitize untrusted HTML first.\n\nOrdinary URL attributes reject executable schemes and unsafe data URLs. Inline `on*` attributes and `iframe srcdoc` are rejected; use function event listeners and an explicitly reviewed raw-HTML path instead.\n\n`renderToString()` emits comment boundaries around dynamic values and keyed `For` regions. `hydrate(root, view)` walks that marker structure, attaches reactive effects and event handlers, and preserves matching elements, rows, and text nodes. It records `data-clank-hydration=\"attached\"` on success.\n\nIf the client tree differs structurally from the server tree, hydration warns, disposes the partial ownership scope, and remounts the root with `data-clank-hydration=\"remounted\"`. Treat that fallback as a development signal: render deterministic initial data and seed live clients from `readState()` before hydration.\n\nOnly structural `HydrationMismatch` failures trigger that fallback. Errors from components, directives, event bindings, or lifecycle callbacks are cleaned up and rethrown so application bugs remain visible instead of running the tree a second time. Clank also removes listeners, effects, and directive cleanups from any partially attached subtree before it remounts.\n\nEvent handlers and `onMount` callbacks do not run during SSR. They attach or run during hydration. Use `serializeState()`/`renderDocument({ state })` rather than interpolating JSON into scripts; it escapes HTML- and script-significant characters.\n\nFor a strict Content Security Policy, generate a fresh nonce per response and pass it to both the policy and document renderer:\n\n```tsx\nconst page = await renderDocument(<App />, {\n  nonce,\n  state,\n  scripts: [\"/app.js\"],\n});\n```\n\nThe nonce is applied to Clank's generated state and module scripts. Inline scripts supplied in `head` must receive the same `nonce` property.\n\n## Compiler escape hatches\n\n`expression(() => value)` manually creates the same reactive boundary emitted by the compiler. `jsx()` and `h()` are public for tooling and generated code. Most application code should not need them.\n"}