Choose an API
One canonical API for each task, plus the patterns you never write in Ilha.
Ilha gives you exactly one way to do each task. Find your task in the table and use that API.
Task to API
| Task | Use | Guide |
|---|---|---|
| Local state | atom(0) |
State management |
| Derived value | atom(Atom.map(source.atom, fn)) |
State management |
| Derived from several atoms | atom(Atom.transform(source.atom, (get) => ...)) |
State management |
| One-time init | atom.lazy(() => expensive()) |
State management |
| React to a change | watch(source, fn) |
State management |
| Mount-only effect | watch.once(fn) |
State management |
| Async data | atom(Effect), read as an AsyncResult |
Data fetching |
| Live data | yield a Stream from a generator component |
Streams |
| Per-emission interruptible flow | yield* when(stream, body) |
Streams |
| Values shared down the tree | createContext() and context() |
Context and errors |
| Failures in a subtree | <ErrorBoundary fallback={...}> |
Context and errors |
| Server render | await renderToString(Component) |
Render and hydrate |
| Client render | mount(host, Component) |
Render and hydrate |
| Hydrate server HTML | mount(host, Component, { hydrate: true }) |
Render and hydrate |
| Raw, trusted HTML | unsafe(html) |
Templating |
Example
State, a derived value, and a reaction use three different APIs. Each task has one.
import * as import AtomAtom from "effect/reactivity/Atom";
import { const atom: AtomFnatom, const watch: WatchFnwatch } from "ilha";
const const Cart: () => ViewCart = () => {
const const items: AtomHandle<{
id: string;
n: number;
}[]>
items = atom<{
id: string;
n: number;
}[]>(init: {
id: string;
n: number;
}[] | Atom.Atom<{
id: string;
n: number;
}[]>, options?: AtomOptions<{
id: string;
n: number;
}[]> | undefined): AtomHandle<{
id: string;
n: number;
}[]> (+1 overload)
An existing Effect atom, or a plain value. Store a function with
`atom.lazy(() => fn)`; the `ilha/oxlint` rule `function-in-atom` flags
`atom(fn)`.atom([
{ id: stringid: "a", n: numbern: 1 },
{ id: stringid: "b", n: numbern: 2 },
]);
const const total: AtomHandle<number>total = atom<number>(init: number | Atom.Atom<number>, options?: AtomOptions<number> | undefined): AtomHandle<number> (+1 overload)An existing Effect atom, or a plain value. Store a function with
`atom.lazy(() => fn)`; the `ilha/oxlint` rule `function-in-atom` flags
`atom(fn)`.atom(
import AtomAtom.const map: <Atom.Atom<{
id: string;
n: number;
}[]>, number>(self: Atom.Atom<{
id: string;
n: number;
}[]>, f: (_: {
id: string;
n: 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<{
id: string;
n: number;
}[]>
items.AtomHandle<{ id: string; n: number; }[]>.atom: Atom.Atom<{
id: string;
n: number;
}[]>
atom, (list: {
id: string;
n: number;
}[]
list) => list: {
id: string;
n: number;
}[]
list.Array<{ id: string; n: number; }>.reduce<number>(callbackfn: (previousValue: number, currentValue: {
id: string;
n: number;
}, currentIndex: number, array: {
id: string;
n: 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((sum: numbersum, item: {
id: string;
n: number;
}
item) => sum: numbersum + item: {
id: string;
n: number;
}
item.n: numbern, 0))
);
watch<number>(source: Atom.Atom<number> | AtomHandle<number> | Stream<number, unknown, unknown>, fn: WatchCallback<number>): Instruction<void>Run `fn` with the current value of `source` now and on every change.watch(const total: AtomHandle<number>total, (value: numbervalue) => {
var localStorage: Storage[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage)localStorage.Storage.setItem(key: string, value: string): voidThe **`setItem()`** method of the Storage interface, when passed a key name and value, will add that key to the given Storage object, or update that key's value if it already exists.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem)setItem("cart:total", var String: StringConstructor
(value?: any) => string
Allows manipulation and formatting of text strings and determination and location of substrings within strings.String(value: numbervalue));
});
return <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>{const total: AtomHandle<number>total}</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>;
};
atom() holds state, Atom.map derives the total, and watch() reacts when it changes. Pass items.atom to Effect APIs, then wrap the result in atom().
Never do
- Never call
atom(),watch(), orwhen()conditionally, in a loop, or after an early return. Slots are order-based, and a changed count triggers ILHA002. - Never use React hooks (
useState,useEffect,useMemo). Ilha has no hook layer. - Never read an atom with
.valueor.get(). Callcount(), or pass{count}in JSX. - Never use an atom handle from another island. It throws ILHA003.
- Never wrap Effect Atom APIs (
Atom.map,Atom.transform,Atom.fn) in your own helper. Passhandle.atomto them. - Never fetch inside
watch.once()and copy the result into an atom. Writeatom(Effect)instead. - Never pass untrusted input to
unsafe(). It skips escaping. - Never import removed APIs:
resource,invalidate,fromEventSource,ilha/define, orstructuralEqual. SeeMIGRATIONS.mdin theilhapackage.
Check your work
Agents get machine-readable feedback at every step:
- Each misuse raises an
IlhaErrorwith a stableILHA00Ncode, afixline, and a link to Errors. ilha/testrenders a component and returns its tree and atoms.ilha/debuginspects live islands as JSON.- The Oxlint plugin catches conditional primitives before you run the code.
- The
ilhapackage shipsAGENTS.md,llms.txt, andMIGRATIONS.md, and the site publishes/llms.txt.