CORE
Components
There is no component API and no lifecycle object. A component is a plain function that returns a Node; props are its arguments; composition is ESM import. A scope has exactly two moments — it is created, and it dies — served by the function body and onCleanup().
A function that returns a Node
// a component is a function that returns a Node; props are its arguments
function Greeting(name) {
return html`<p>hello, ${name}</p>`;
}
// composition is import + a call inside a hole
import { Greeting } from './greeting.js';
const App = () => html`<main>${Greeting(user)}</main>`;
Props are arguments
Pass a value for something fixed, a signal for something that changes. The component does not care which — a hole handles both — and TypeScript types the parameter like any other.
// a plain value: written once
Greeting('world');
// a signal: the <p> follows it
const name = signal('world');
Greeting(name);
// a derived value: pass a computed, or a function
Greeting(computed(() => first.value + ' ' + last.value));
It runs once
A component function runs when it is called and never again. After that only the hole effects are alive, each watching its own signal. There is no re-render to optimise, no memo, no dependency array.
function Counter() {
console.log('runs exactly once'); // not on every click — there is no re-render
const count = signal(0);
return html`<button onclick=${() => count.value++}>${count}</button>`;
}
Conditionals
A function hole that returns a different node is a conditional. The runtime swaps the nodes and disposes the branch that left.
const App = () => html`<div>
${() => (loggedIn.value ? Dashboard(user) : LoginForm())}
</div>`;
// when loggedIn flips, the old branch's scope is disposed — its effects, cleanups, timers — and the new one is built
Lifetime
Everything a component creates — effects, computeds, child components — belongs to it and is disposed with it. onCleanup is where you release what the platform handed you; onMount is the moment its nodes are in the document.
function Clock() {
const now = signal(new Date());
const id = setInterval(() => (now.value = new Date()), 1000);
onCleanup(() => clearInterval(id)); // the scope dies → the interval dies
onMount(() => console.log('in the document now'));
return html`<time>${() => now.value.toLocaleTimeString()}</time>`;
}
Why no component API
Any registry, class or component object would be something the compiler had to understand and amo eject had to reproduce. A function is already what a person writing vanilla would write, so compiled output stays within 10% of hand-written bytes and ejected output stays readable.