Skip to content
Ilha
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.
@stabilityunstable@categorycombinators@since4.0.0
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.
@paramcallbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array.@paraminitialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.
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 generator body for 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.

Was this page helpful?