Basic: mount one component.
This path proves the component model before a server is involved. You define markup once, retain
named nodes with data-taipa-ref, and connect state to those nodes with direct DOM writes.
Define a counter
import { component, html } from "@taipa/ui";
export const Counter = component("Counter", { contractVersion: "1" })
.state("count", 0)
.bind("count", ({ element, state }) => {
element.textContent = String(state.count());
})
.on("increment@click", ({ state }) => {
state.count(state.count() + 1);
})
.on("decrement@click", ({ state }) => {
state.count(state.count() - 1);
})
.render(
({ state }) => html`
<section aria-label="Counter">
<button data-taipa-ref="decrement">Decrease</button>
<output data-taipa-ref="count">${state.count()}</output>
<button data-taipa-ref="increment">Increase</button>
</section>
`,
);bind() and ref-targeted on() declarations make those refs required at hydration time. They also
make callback-side refs.one("count") name-checked in TypeScript.
Mount it once
Start with an empty host in your page.
<div id="counter"></div>import { mount } from "@taipa/ui/client";
import { Counter } from "./counter";
const host = document.querySelector("#counter");
if (host instanceof HTMLElement) {
await mount(host, Counter, { state: { count: 3 } });
}mount() renders once, installs the result through a native template, and then uses the same
direct-DOM attachment path as hydration. Updating count does not render a second tree.
Try it
Mount the counter in this guide
These buttons belong to the same component from the example above.
JavaScript mounts the counter into this empty host.
When the interaction feels right, render this Counter on the server.