Client rendering
Add browser behavior with an explicit use client module. Start with server HTML, attach to existing nodes, then update only the output that depends on changed state.
On this page
The first counter is in the shell. The second arrives through a server boundary. Both use the same browser module, but each owns its own state. Edit a name and increment a count.
Local counter
Hello, .
Double:
Loading another counter…
Start a .tsrx module with "use client" and export a named default function. Setup runs once on the server and once when this root activates in the browser. Pass JSON-shaped props from a server template; return one ordinary HTML root element.
"use client";
import { state, derive } from "renku/client";
export default function Counter({ initial }: { initial: number }) {
const count = state(initial);
const name = state("renku");
const doubled = derive(() => count.get() * 2);
return @{
<section>
<label>Name <input bindValue={name} /></label>
<p>Hello, {name.get()}.</p>
<button onClick={() => count.set(count.get() + 1)}>
Add one
</button>
<output>{count.get()}</output>
<p>Double: {doubled()}</p>
</section>
};
}The behavior used by both counters above, with styles and labels simplified.
state(initial) returns get() and set(value). derive(read) is lazy and cached. Event handlers batch related writes synchronously; Object.is-equal values do not publish. Replace arrays or objects when changing them.
Template expressions become reactive bindings. A component's setup does not rerun on each change. For changing nested props, read props inside template expressions or derive; destructuring during setup takes an initial snapshot.
Use bindValue for non-file inputs and textareas, and bindChecked for checkbox or radio state. Author client textareas without children and supply their text through bindValue or value. Activation adopts early browser edits without replaying events, preserving nodes, focus and selection. Buttons with onClick stay disabled until activation.
Use stable, unique string or number keys when a list can move. This example reverses the rows while retaining each input's node and local draft. Where available, native moveBefore preserves browser-managed state during moves. Other browsers use insertBefore while retaining node identity and local drafts; browser-managed state such as focus may reset. Removing a row disposes its owned work. @if creates and disposes conditional branches using the same lifetime model.
"use client";
import { state } from "renku/client";
function Row({ id }: { id: number }) {
const draft = state("Draft " + id);
return @{ <li><input aria-label={"Draft " + id} bindValue={draft} /></li> };
}
export default function Drafts() {
const ids = state([1, 2, 3]);
return @{
<section>
<button onClick={() => ids.set([...ids.get()].reverse())}>
Reverse rows
</button>
<ul>@for (id of ids.get(); key id) { <Row id={id} /> }</ul>
</section>
};
}Use onMount for browser-only subscriptions. An action captures the component owner and becomes inert after disposal. Remove subscriptions with onCleanup. Cleanup registered in setup also runs after server serialization, so start external resources inside onMount.
"use client";
import { state, action, onMount, onCleanup } from "renku/client";
export default function Clock() {
const seconds = state(0);
const tick = action(() => seconds.set(seconds.get() + 1));
onMount(() => {
const timer = setInterval(tick, 1000);
onCleanup(() => clearInterval(timer));
});
return @{ <output>{seconds.get()} seconds mounted</output> };
}Supported events are onClick, onInput, onChange, onSubmit and onKeyDown. Import the structural KeyboardEvent type from renku/client when typing a keyboard handler; check isComposing before handling a text-entry shortcut. Setup, derived reads and render bindings cannot write owned state; runtime guards catch captured actions too. Browser application modules must explicitly opt in with "use client"; shared .stylex.ts tokens are allowed. Node and Worker runtime imports stay out of browser bundles.
Async client components, typed async/context requirements, DOM refs, event replay and retained routing are not implemented. Native HTML tables support reactive cells and keyed rows. Author explicit thead, tbody and tfoot sections so the browser preserves the server DOM during activation. Keep selects, foreign markup, text-only elements and server boundaries outside client templates. The Worker authoring environment does not provide browser globals. An import or markup mismatch preserves server content and offers a reload.
Continue with server streaming →You can now attach independent browser state to server HTML and clean up owned work when a root is removed.