Core package reference
Public exports from the ilha package.
Use this page as a map of the public ilha API. Start with Introduction if you are building your first component.
Component
A component is a function, async function, or generator that returns a view.
import { const atom: AtomFnatom } from "ilha";
const const Status: () => ViewStatus = () => <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>Ready</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>;
const const Greeting: ({ name }: {
name: string;
}) => View
Greeting = ({ name: stringname }: { name: stringname: string }) => <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>Hello, {name: stringname}</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>;
const const Counter: () => ViewCounter = () => {
const const count: AtomHandle<number>count = atom<number>(init: number | Atom<number> | Effect<number, unknown, AtomRegistry> | Stream<number, unknown, AtomRegistry>, options?: AtomOptions<number> | undefined): AtomHandle<number>atom(0);
return (
<JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton type?: string | undefinedtype="button" EventProps<HTMLButtonElement>.onclick?: ((event: Targeted<HTMLButtonElement, MouseEvent>) => void) | undefinedonclick={() => const count: AtomHandle<number>count.AtomHandle<number>.update: (f: (current: number) => number) => voidupdate((n: numbern) => n: numbern + 1)}>
{const count: AtomHandle<number>count}
</JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton>
);
};
Nested functions share the parent fiber. mount / renderToString on a function makes it a root.
Named exports
| Export | Purpose | Learn more |
|---|---|---|
atom(init, { equals? }) |
Writable reactive value; skip notify on equal writes | State |
atom.lazy(init) |
One-time initializer | State |
resource(key, fetcher, { staleWhileRevalidate? }) |
Shared async fetch with dedupe + SWR | State |
invalidate(key) |
Drop a cached resource and refetch live users | State |
fromEventSource(url, { event?, schema?, retryBaseMs? }) |
SSE stream, status, and latest value | Streams |
batch(fn) |
Coalesce multiple atom writes | State |
untrack(fn) |
Read atoms without subscribing | State |
createContext / context |
Atom-backed tree context | Context and errors |
ErrorBoundary |
Catch subtree failures | Context and errors |
watch(source, fn) |
Callback on atom or stream changes; receives { signal, onCleanup } |
State |
watch.once(fn) |
Run once on mount; async callbacks with signal + promised cleanup | State |
when(stream, body) |
Per-emission generator (interrupts stale) | Streams |
h / Fragment |
JSX factory | Templating |
mount |
Activate a component in the DOM | Render |
renderToString |
Serialize a component to HTML | Render |
Element props
Every element accepts key and ref with no extra types. key reuses DOM nodes in lists; ref receives the live node on mount and null on unmount. Components accept key through JSX.IntrinsicAttributes — never add it to your own props. Disclosure and dialog events (ontoggle, onbeforetoggle, onclose, oncancel) type currentTarget to the element.
data-morph-preserve="a b" keeps the named live attributes across rerenders; the children token keeps the element’s live children. A user-opened <details> or <dialog> keeps open unless the render sets it explicitly. See Templating.
Paint Effect Stream values with yield Stream.map(...) — see Streams.
define lives at ilha/define — see Custom elements.
atom
| Call | Meaning |
|---|---|
atom(value) |
Writable atom |
atom.lazy(fn) |
Run fn once on init |
atom(effectAtom) |
Wrap an Effect atom |
count() |
Read |
count.set(v) |
Replace |
count.update(fn) |
Patch |
count.atom |
Underlying Effect atom |
Derived values use Effect’s Atom.map or Atom.transform — pass handle.atom, then wrap in atom():
import * as import AtomAtom from "effect/reactivity/Atom";
import { const atom: AtomFnatom } from "ilha";
const const items: AtomHandle<number[]>items = atom<number[]>(init: number[] | Atom.Atom<number[]> | Effect<number[], unknown, AtomRegistry> | Stream<number[], unknown, AtomRegistry>, options?: AtomOptions<number[]> | undefined): AtomHandle<number[]>atom([1, 2]);
const const sum: AtomHandle<number>sum = atom<number>(init: number | Atom.Atom<number> | Effect<number, unknown, AtomRegistry> | Stream<number, unknown, AtomRegistry>, options?: AtomOptions<number> | undefined): AtomHandle<number>atom(
import AtomAtom.const map: <Atom.Atom<number[]>, number>(self: Atom.Atom<number[]>, f: (_: number[]) => number) => Atom.Atom<number> (+1 overload)Maps the current value of an atom with a pure function.
**Details**
When the source atom is writable, the returned atom remains writable and keeps
the source atom's write input type.map(const items: AtomHandle<number[]>items.AtomHandle<number[]>.atom: Atom.Atom<number[]>atom, (list: number[]list) => list: number[]list.Array<number>.reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number (+2 overloads)Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((total: numbertotal, n: numbern) => total: numbertotal + n: numbern, 0))
);
Atoms from Effect
The initializer also accepts an Effect atom, an Effect, or a Stream:
const const status: AtomHandle<string>status = atom<string>(init: string | Atom<string> | Effect.Effect<string, unknown, AtomRegistry> | Stream<string, unknown, AtomRegistry>, options?: AtomOptions<string> | undefined): AtomHandle<string>atom(const effect: Effect.Effect<string, never, never>effect);
The value of an Effect-backed atom is an Effect AsyncResult. During paint, waiting results keep the previous view, failures render the error text, and successes render the value. Use this to hold async data in atoms; for streams of views, prefer Streams.
watch
Order-based primitive — call at the top level of a component. watch(source, fn) runs fn when source changes and once on mount. Accepts an atom handle, handle.atom, or a Stream. watch.once(fn) runs fn once on mount; return a function to clean up on unmount.
Both forms pass { signal, onCleanup }: the signal aborts when a run goes stale or the component unmounts, and callbacks may be async with a promised cleanup.
when
Generator instruction — use with yield* inside a generator component. Prefer yielding a Stream of views directly when you do not need per-emission interruption.
when(stream, body)— run generatorbodyfor each value; a new value interrupts the previous body. SSR takes the first value.
Stream views (yield Stream.map(...)) also take one value on SSR. Side effects and user-gated flows use Effect (Stream.runForEach, Deferred) — see Streams.
renderToString
await renderToString(component, {
snapshot?: boolean; // embed atom values for hydration (default true)
markers?: boolean; // wrap in <div data-ilha> (default true)
timeout?: number; // serialize after this many ms even if busy
captureActions?: boolean; // probe handlers for server-action calls
});
Waits until in-flight work is idle, then serializes. when and Stream views take one value.
mount
const unmount = mount(element, component, {
hydrate?: boolean; // restore snapshots from existing markup
onError?: (error: Error) => void; // component failure hook
});
onError receives a failing root component and a nested component no ErrorBoundary caught, before the error view paints.
ilha/renderer
createRenderer(ops) paints components into a host that is not a DOM: a native toolkit, a canvas scene graph, a test tree. You supply PaintOps; atoms, generators, streams, contexts, keyed components, and error boundaries behave as they do under mount().
import { createRenderer } from "ilha/renderer";
import type { PaintOps } from "ilha/renderer";
const renderer = createRenderer(ops);
const { unmount, ready, runtime } = renderer.mount(root, App, {
onError?: (error: Error) => void; // component failure hook
onRuntime?: (runtime: IlhaRuntime) => void; // runs before the root component
});
Every paint materializes fresh host nodes and hands the complete child list of the painted root to ops.placeChildren(root, nodes). Your host decides what to keep.
| Op | Role |
|---|---|
createElement, createText |
Build a host node |
createRaw(html, parent) |
Nodes for an unsafe() view |
createSlotHost(slotId) |
Host of a component, atom, or stream hole |
asRoot, asNode, asElement |
View a host as a fiber root, a placeable node, or an element |
placeChildren(root, nodes) |
Make a root’s children exactly nodes |
appendRoot(root, node) |
Append one node. Only painter.insert calls it, never mount() |
reuseNode(host) |
Node to place for a hole that survived the repaint |
setProp(el, key, value) |
Optional. Return true to claim a prop with its original value |
setStyle, setFormControl |
Default handling for style and value / checked |
bindEvents(el, props, fiber) |
Bind event props; call the exported bindEvents to reuse Ilha’s |
disconnect(el) |
A hole’s host left the tree for good |
committed(el) |
The element a rendered element ended up as, for refs |
Every op except setProp is required.
Without setProp, a prop reaches the host as a string attribute and a style object as CSS text. Implement it to receive objects as written.
onRuntime hands you the mount’s runtime before any component runs. Every fiber of the mount carries it, so getFiber().runtime identifies the mount from inside a component.
Types
Component, View, AtomHandle, MountOptions, RenderToStringOptions.