Build interactive pages with Ilha
Write Ilha components, state, events, and islands in app pages.
Call atom and watch at the top level of each component, in the same order on every render. Never call them inside if, loops, callbacks, or after await.
import * as Atom from "effect/reactivity/Atom";
import { atom, watch } from "ilha";
export default function Counter({ start = 0 }: { start?: number }) {
const count = atom(start); // read count(), write count.set(1)
const doubled = atom(Atom.map(count.atom, (n) => n * 2)); // derived
const startedAt = atom.lazy(() => Date.now()); // per-instance init
watch.once(() => {
// mount-only side effect; return a cleanup if needed
});
return (
<button type="button" onclick={() => count.update((n) => n + 1)}>
{count} / {doubled} (since {startedAt})
</button>
);
}
Follow the atom rules
- Pass
atoma value, an Effect Atom, an Effect, or a Stream. - Never pass a plain function; use
atom.lazy(() => …)for init or stored functions. - Pass
handle.atomto Effect APIs that expect an atom handle. - Derive state with
atom(Atom.map(source.atom, (n) => …)). - Give every component returned from
.map()a stablekey. - Use
yield* when(…)only inside generator components. - Use lowercase DOM names:
onclick,class,for. - Keep island props JSON; never put JSX in atoms.
Render islands on island pages
- Name the page
*.island.tsxfor static HTML with hydrated islands only. - Import islands with
import X from "./_x" with { island: "visible" }. - Use
useIslandForm()from@ilha/voidon island pages, notuseForm(). - Assert server output with
renderHtml(Component)fromilha/test.
Read the error code first
Every Ilha error carries a code such as ILHA002, a Fix: line, and a link. Read the fix before changing code.
For the person-side walkthrough, see Add interactive pages.