State management
Declare reactive values with atom(), derive with Effect Atom.map and Atom.transform, and react with watch().
atom()
Declare local state with atom(). Reads during render subscribe the component; writes rerun it.
import { const atom: AtomFnatom } from "ilha";
const const Counter: () => ViewCounter = () => {
const const count: AtomHandle<number>count = atom<number>(init: number | Atom<number> | Effect<number, any, any> | Stream<number, any, any>): AtomHandle<number>atom(0);
return (
<IntrinsicElements[string]: anybutton
type: stringtype="button"
onclick: () => voidonclick={() => const count: AtomHandle<number>count.AtomHandle<number>.update(f: (current: number) => number): voidupdate((n: numbern: number) => n: numbern + 1)}
>
{const count: AtomHandle<number>count}
</IntrinsicElements[string]: anybutton>
);
};
| Call | Meaning |
|---|---|
count() |
Read the current value |
{count} in JSX |
Read and subscribe |
count.set(1) |
Replace |
count.update((n) => n + 1) |
Patch from previous |
Where you read matters
A read in the component body (count() or String(count())) subscribes the whole component to rerun. Passing the handle in JSX ({count}) gives that node its own subscription and updates only that subtree. Prefer {count} in markup when you do not need the value in logic.
In async components, declare atoms before the first await. After an await, pass handles in JSX instead of calling them in the body — only the synchronous setup pass collects render dependencies.
Derived atoms
Pass handle.atom to Effect, wrap the result in atom():
import * as import AtomAtom from "effect/unstable/reactivity/Atom";
import { const atom: AtomFnatom } 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;
}[]> | Effect<{
id: string;
n: number;
}[], any, any> | Stream<{
id: string;
n: number;
}[], any, any>): AtomHandle<{
id: string;
n: number;
}[]>
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> | Effect<number, any, any> | Stream<number, any, any>): AtomHandle<number>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),
),
);
return <IntrinsicElements[string]: anyp>{const total: AtomHandle<number>total}</IntrinsicElements[string]: anyp>;
};
| API | Use when |
|---|---|
Atom.map(source, f) |
Pure function of one atom |
Atom.transform(source, (get, source) => …) |
Reads more than one atom via get |
import * as import AtomAtom from "effect/unstable/reactivity/Atom";
import { const atom: AtomFnatom } from "ilha";
const const Form: () => ViewForm = () => {
const const first: AtomHandle<string>first = atom<string>(init: string | Atom.Atom<string> | Effect<string, any, any> | Stream<string, any, any>): AtomHandle<string>atom("");
const const last: AtomHandle<string>last = atom<string>(init: string | Atom.Atom<string> | Effect<string, any, any> | Stream<string, any, any>): AtomHandle<string>atom("");
const const fullName: AtomHandle<string>fullName = atom<string>(init: string | Atom.Atom<string> | Effect<string, any, any> | Stream<string, any, any>): AtomHandle<string>atom(
import AtomAtom.const transform: <Atom.Atom<string>, string>(self: Atom.Atom<string>, f: (get: Atom.AtomContext, atom: Atom.Atom<string>) => string, options?: {
readonly initialValueTarget?: Atom.Atom<string> | undefined;
} | undefined) => Atom.Atom<string> (+1 overload)
Creates a derived atom by reading another atom with a custom `AtomContext`
function.
**Details**
If the source is writable, the derived atom keeps the source write input and
forwards writes to the source. `initialValueTarget` controls which atom receives
preloaded initial values for the derived atom.transform(const first: AtomHandle<string>first.AtomHandle<string>.atom: Atom.Atom<string>atom, (get: Atom.AtomContextget) =>
`${get: Atom.AtomContext
<string>(atom: Atom.Atom<string>) => string
get(const first: AtomHandle<string>first.AtomHandle<string>.atom: Atom.Atom<string>atom)} ${get: Atom.AtomContext
<string>(atom: Atom.Atom<string>) => string
get(const last: AtomHandle<string>last.AtomHandle<string>.atom: Atom.Atom<string>atom)}`.String.trim(): stringRemoves the leading and trailing white space and line terminator characters from a string.trim(),
),
);
return <IntrinsicElements[string]: anyp>{const fullName: AtomHandle<string>fullName}</IntrinsicElements[string]: anyp>;
};
Side effects
Render subscription reruns the component — it is not useEffect. Use watch() for callbacks on atom changes:
import { const atom: AtomFnatom, function watch<A>(source: AtomHandle<A> | Atom<A> | Stream<A, any, any>, fn: (value: A) => void): Instruction<void>Run `fn` when `source` changes — and once on mount. Sync, async, and generator components.watch } from "ilha";
const const Profile: () => ViewProfile = () => {
const const name: AtomHandle<string>name = atom<string>(init: string | Atom<string> | Effect<string, any, any> | Stream<string, any, any>): AtomHandle<string>atom("john");
watch<string>(source: Atom<string> | Stream<string, any, any> | AtomHandle<string>, fn: (value: string) => void): Instruction<void>Run `fn` when `source` changes — and once on mount. Sync, async, and generator components.watch(const name: AtomHandle<string>name, (value: stringvalue) => {
var document: Document**`window.document`** returns a reference to the document contained in the window.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)document.Document.title: stringThe **`document.title`** property gets or sets the current title of the document. When present, it defaults to the value of the <title>.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/title)title = value: stringvalue;
});
return (
<IntrinsicElements[string]: anyinput
value: AtomHandle<string>value={const name: AtomHandle<string>name}
oninput: (e: Event) => voidoninput={(e: Evente: Event) =>
const name: AtomHandle<string>name.AtomHandle<string>.set(next: string): voidset((e: Evente.Event.currentTarget: EventTarget | nullThe **`currentTarget`** read-only property of the Event interface identifies the element to which the event handler has been attached.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/currentTarget)
Alias for event.target.currentTarget as HTMLInputElement).HTMLInputElement.value: stringThe **`value`** property of the HTMLInputElement interface represents the current value of the <input> element as a string.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/value)value)
}
/>
);
};
Call watch() before any await in async components. Guard DOM access on the server. For streams and when, see Streams.
Batching writes
batch() notifies subscribers once after multiple writes:
import { const atom: AtomFnatom, const batch: (f: () => void) => voidbatch } from "ilha";
const const Panel: () => ViewPanel = () => {
const const a: AtomHandle<number>a = atom<number>(init: number | Atom<number> | Effect<number, any, any> | Stream<number, any, any>): AtomHandle<number>atom(0);
const const b: AtomHandle<number>b = atom<number>(init: number | Atom<number> | Effect<number, any, any> | Stream<number, any, any>): AtomHandle<number>atom(0);
const const reset: () => voidreset = () => {
function batch(f: () => void): voidbatch(() => {
const a: AtomHandle<number>a.AtomHandle<number>.set(next: number): voidset(0);
const b: AtomHandle<number>b.AtomHandle<number>.set(next: number): voidset(0);
});
};
return (
<IntrinsicElements[string]: anybutton type: stringtype="button" onclick: () => voidonclick={const reset: () => voidreset}>
{const a: AtomHandle<number>a}-{const b: AtomHandle<number>b}
</IntrinsicElements[string]: anybutton>
);
};
Async mutations
atom(Atom.fn(...)) runs an Effect when you call .set():
import * as import AtomAtom from "effect/unstable/reactivity/Atom";
import * as import EffectEffect from "effect/Effect";
import { const atom: AtomFnatom } from "ilha";
const const Form: () => ViewForm = () => {
const const save: AtomHandle<AsyncResult<any, unknown>>save = atom<AsyncResult<any, unknown>>(init: AsyncResult<any, unknown> | Atom.Atom<AsyncResult<any, unknown>> | Effect.Effect<AsyncResult<any, unknown>, any, any> | Stream<AsyncResult<any, unknown>, any, any>): AtomHandle<AsyncResult<any, unknown>>atom(
import AtomAtom.const fn: <unknown, any, string>(fn: (arg: string, get: Atom.FnContext) => Effect.Effect<any, unknown, Scope | AtomRegistry>, options?: {
readonly initialValue?: any;
readonly concurrent?: boolean | undefined;
} | undefined) => Atom.AtomResultFn<string, any, unknown> (+3 overloads)
Creates a writable atom for an `Effect` or `Stream` function; writing an argument starts the computation and exposes its state as an `AsyncResult`.fn((email: stringemail: string) =>
import EffectEffect.const tryPromise: <any, unknown>(options: ((signal: AbortSignal) => PromiseLike<any>) | {
readonly try: (signal: AbortSignal) => PromiseLike<any>;
readonly catch: (error: unknown) => unknown;
}) => Effect.Effect<any, unknown, never>
Creates an `Effect` from an asynchronous computation that may throw or
reject, mapping failures into the error channel.
**When to use**
Use when you need to perform asynchronous operations that might fail, such
as fetching data from an API, and want thrown exceptions or rejected promises
captured as Effect errors.
**Details**
The promise thunk is evaluated when the effect runs. If it returns a promise
that resolves, the resolved value becomes the success value. If the thunk
throws before returning a promise, or if the returned promise rejects, the
thrown or rejected value is mapped into the error channel.
Passing the thunk directly maps failures to
{@link
Cause.UnknownError
}
.
Passing `{ try, catch }` uses `catch` to map failures to an error of type
`E`.
The thunk receives an `AbortSignal` that is aborted if the effect is
interrupted. The underlying asynchronous operation only stops if it observes
that signal.
**Gotchas**
If `catch` throws while mapping the error, that thrown value is treated as a
defect. Return the error value you want in the error channel instead of
throwing it.
**Example** (Wrapping a fetch request that may fail)
```ts import.meta.vitest
import { Effect } from "effect"
const getTodo = (id: number) =>
Effect.tryPromise(() => Promise.resolve({ id, completed: false }))
// ┌─── Effect<{ id: number; completed: boolean }, UnknownError, never>
// ▼
const program = getTodo(1)
await Effect.runPromise(program) // => { id: 1, completed: false }
```
**Example** (Mapping Promise rejections to a tagged error)
```ts import.meta.vitest
import { Data, Effect } from "effect"
class TodoFetchError extends Data.TaggedError("TodoFetchError")<{ readonly cause: unknown }> {}
const getTodo = (id: number) =>
Effect.tryPromise({
try: () => Promise.reject(`Todo ${id} is unavailable`),
// remap the error
catch: (cause) => new TodoFetchError({ cause })
})
// ┌─── Effect<never, TodoFetchError, never>
// ▼
const program = Effect.flip(getTodo(1))
const error = await Effect.runPromise(program)
error._tag // => "TodoFetchError"
```tryPromise({
try: (signal: AbortSignal) => PromiseLike<any>try: () =>
function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+3 overloads)[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch)fetch("/api/waitlist", {
RequestInit.method?: string | undefinedA string to set request's method.method: "POST",
RequestInit.body?: BodyInit | null | undefinedA BodyInit object or null to set request's body.body: var JSON: JSONAn intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.JSON.JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)Converts a JavaScript value to a JavaScript Object Notation (JSON) string.stringify({ email: stringemail }),
}).Promise<Response>.then<any, never>(onfulfilled?: ((value: Response) => any) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<any>Attaches callbacks for the resolution and/or rejection of the Promise.then((r: Responser) => r: Responser.Body.json(): Promise<any>[MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json)json()),
catch: (error: unknown) => unknowncatch: (e: unknowne) => e: unknowne,
}),
),
);
return (
<IntrinsicElements[string]: anyform
onsubmit: (e: SubmitEvent) => voidonsubmit={(e: SubmitEvente: SubmitEvent) => {
e: SubmitEvente.Event.preventDefault(): void (+1 overload)Sets the `defaultPrevented` property to `true` if `cancelable` is `true`.preventDefault();
const const email: stringemail = var String: StringConstructor
(value?: any) => string
Allows manipulation and formatting of text strings and determination and location of substrings within strings.String(
new var FormData: new (form?: HTMLFormElement, submitter?: HTMLElement | null) => FormDataThe **`FormData`** interface provides a way to construct a set of key/value pairs representing form fields and their values, which can be sent using the fetch(), XMLHttpRequest.send() or navigator.sendBeacon() methods. It uses the same format a form would use if the encoding type were set to "multipart/form-data".
[MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData)FormData(e: SubmitEvente.Event.currentTarget: EventTarget | nullThe **`currentTarget`** read-only property of the Event interface identifies the element to which the event handler has been attached.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/currentTarget)
Alias for event.target.currentTarget as HTMLFormElement).FormData.get(name: string): Bun.FormDataEntryValue | null (+1 overload)[MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/get)get(
"email",
) ?? "",
);
if (import AtomAtom.const isWritable: <AsyncResult<any, unknown>, unknown>(atom: Atom.Atom<AsyncResult<any, unknown>>) => atom is Atom.Writable<AsyncResult<any, unknown>, unknown>Returns `true` when an atom is writable.isWritable(const save: AtomHandle<AsyncResult<any, unknown>>save.AtomHandle<AsyncResult<any, unknown>>.atom: Atom.Atom<AsyncResult<any, unknown>>atom))
const save: AtomHandle<AsyncResult<any, unknown>>save.AtomHandle<AsyncResult<any, unknown>>.set(next: AsyncResult<any, unknown>): voidset(const email: stringemail as never);
}}
>
<IntrinsicElements[string]: anyinput
name: stringname="email"
type: stringtype="email"
placeholder: stringplaceholder="you@company.com"
/>
<IntrinsicElements[string]: anybutton type: stringtype="submit">Join waitlist</IntrinsicElements[string]: anybutton>
</IntrinsicElements[string]: anyform>
);
};
For optimistic updates, use Atom.optimisticFn.
Lazy initialization
atom.lazy(() => …) runs once per slot. Use it for expensive init or to store a function value — not atom(fn).
import { const atom: AtomFnatom } from "ilha";
const const Panel: () => ViewPanel = () => {
const const settings: AtomHandle<any>settings = const atom: AtomFnatom.AtomFn.lazy<any>(init: () => any): AtomHandle<any>lazy(() => {
if (typeof var localStorage: Storage[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage)localStorage === "undefined") return {};
return var JSON: JSONAn intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.JSON.JSON.parse(text: string, reviver?: (this: any, key: string, value: any) => any): anyConverts a JavaScript Object Notation (JSON) string into an object.parse(var localStorage: Storage[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage)localStorage.Storage.getItem(key: string): string | nullThe **`getItem()`** method of the Storage interface, when passed a key name, will return that key's value, or null if the key does not exist, in the given Storage object.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/getItem)getItem("prefs") ?? "{}");
});
return <IntrinsicElements[string]: anyp>{var JSON: JSONAn intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.JSON.JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)Converts a JavaScript value to a JavaScript Object Notation (JSON) string.stringify(const settings: AtomHandle
() => any
settings())}</IntrinsicElements[string]: anyp>;
};
Props vs state
State initialized from props does not reset when props change. Atoms hold data, not JSX.