# Rendering and components
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.
## TSX components
```tsx
interface BadgeProps {
tone: "green" | "amber";
children: Renderable | Renderable[];
}
function Badge(props: BadgeProps) {
return {props.children};
}
function Status() {
return (
Ready
<>One fragment can contain several siblings.>
);
}
```
Lowercase tags create HTML/SVG elements. Uppercase or member-expression tags call components. Fragments create no wrapper element.
`h(type, props, ...children)` remains available as a lower-level integration API, but application code normally uses TSX.
## Automatic reactivity
Any nonliteral expression in a TSX child or ordinary element property is compiled into a lazy reactive marker:
```tsx
const count = signal(0);
const doubled = computed(() => count.value * 2);
function Counter() {
return (
);
}
```
The 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.
Component prop expressions are lazy too:
```tsx
function Greeting(props: { name: string }) {
return
Hello {props.name}
;
}
```
The runtime exposes expression-backed props through getters. Reading `props.name` inside the heading's compiled expression observes the latest parent value.
JavaScript executed imperatively in the component body still runs once. Use TSX expressions, `Show`, `Switch`, or an `effect` for behavior that must react.
## Classes and styles
```tsx
```
Dynamic `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.
## Events
```tsx
```
Event 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.
## Forms
`bind:` creates a two-way connection to a signal:
```tsx
const email = signal("");
const accepted = signal(false);
{email.value}
```
`bind:value` listens to `input`; other bound properties listen to `change`. The compiler passes the signal itself rather than wrapping it.
For 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).
Clank's strict JSX declarations type native tags, element properties, event `currentTarget`, reactive values, ARIA/data attributes, bind/ref/use protocols, and hyphenated custom elements.
## Refs and directives
```tsx
const input = signal(null);
{
const observer = new ResizeObserver(report);
observer.observe(element);
return () => observer.disconnect();
}}
/>
```
`ref` accepts a callback or signal. `use` accepts one directive or an array. A directive may return an unmount cleanup.
## Lifecycle and context
```tsx
const Theme = createContext("system");
function Shell() {
provideContext(Theme, "dark");
onMount(() => {
console.log("mounted");
return () => console.log("unmounted");
});
return ;
}
function Page() {
const theme = useContext(Theme);
return
Theme: {theme}
;
}
```
Providers 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.
## Conditional control flow
```tsx
}>
ReadyFailed
```
Only the selected dynamic region is mounted. Changing the condition disposes the previous region before mounting the next.
## Keyed lists
```tsx
No tasks}>
{(task, index) => (
{index() + 1}. {task.title}
)}
```
`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.
Use `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.
## Lazy components and promises
`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.
## Raw HTML and hydration
`dangerouslySetInnerHTML={{ __html }}` is deliberately explicit and bypasses child mounting. Sanitize untrusted HTML first.
Ordinary 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.
`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.
If 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.
Only 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.
Event 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.
For a strict Content Security Policy, generate a fresh nonce per response and pass it to both the policy and document renderer:
```tsx
const page = await renderDocument(, {
nonce,
state,
scripts: ["/app.js"],
});
```
The nonce is applied to Clank's generated state and module scripts. Inline scripts supplied in `head` must receive the same `nonce` property.
## Compiler escape hatches
`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.