State management
Declare reactive values with atom(), derive with Effect Atom.map and Atom.transform, and react with watch() or watch.once().
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, 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>
);
};
| 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.
Use untrack(() => …) to read atoms during render without subscribing:
import { const atom: AtomFnatom, const untrack: <A>(fn: () => A) => ARead atoms inside `fn` without subscribing the active render. Sync only.untrack } from "ilha";
const const Panel: () => ViewPanel = () => {
const const a: AtomHandle<number>a = atom<number>(init: number | Atom<number> | Effect<number, unknown, AtomRegistry> | Stream<number, unknown, AtomRegistry>, options?: AtomOptions<number> | undefined): AtomHandle<number>atom(0);
const const b: AtomHandle<number>b = atom<number>(init: number | Atom<number> | Effect<number, unknown, AtomRegistry> | Stream<number, unknown, AtomRegistry>, options?: AtomOptions<number> | undefined): AtomHandle<number>atom(10);
const const peeked: numberpeeked = untrack<number>(fn: () => number): numberRead atoms inside `fn` without subscribing the active render. Sync only.untrack(() => const b: AtomHandle
() => number
b());
return (
<JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>
{const a: AtomHandle<number>a}:{const peeked: numberpeeked}
</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>
);
};
untrack is sync only. Prefer {handle} or a derived atom(Atom.map(...)) when you need the view to update.
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/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;
}[], unknown, AtomRegistry> | Stream<{
id: string;
n: number;
}[], unknown, AtomRegistry>, options?: AtomOptions<{
id: string;
n: number;
}[]> | undefined): 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, unknown, AtomRegistry> | Stream<number, unknown, AtomRegistry>, options?: AtomOptions<number> | undefined): 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 <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>{const total: AtomHandle<number>total}</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>;
};
| 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/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, unknown, AtomRegistry> | Stream<string, unknown, AtomRegistry>, options?: AtomOptions<string> | undefined): AtomHandle<string>atom("");
const const last: AtomHandle<string>last = atom<string>(init: string | Atom.Atom<string> | Effect<string, unknown, AtomRegistry> | Stream<string, unknown, AtomRegistry>, options?: AtomOptions<string> | undefined): AtomHandle<string>atom("");
const const fullName: AtomHandle<string>fullName = atom<string>(init: string | Atom.Atom<string> | Effect<string, unknown, AtomRegistry> | Stream<string, unknown, AtomRegistry>, options?: AtomOptions<string> | undefined): 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 <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>{const fullName: AtomHandle<string>fullName}</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>;
};
Side effects
Render subscription reruns the component — it is not useEffect. Use watch() for callbacks on atom changes:
import { const atom: AtomFnatom, const watch: WatchFnwatch } from "ilha";
const const Profile: () => ViewProfile = () => {
const const name: AtomHandle<string>name = atom<string>(init: string | Atom<string> | Effect<string, unknown, AtomRegistry> | Stream<string, unknown, AtomRegistry>, options?: AtomOptions<string> | undefined): AtomHandle<string>atom("john");
watch<string>(source: Atom<string> | AtomHandle<string> | Stream<string, unknown, unknown>, fn: WatchCallback<string>): Instruction<undefined>watch(const name: AtomHandle<string>name, (value: stringvalue) => {
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("profile:name", value: stringvalue);
});
return (
<JSX.IntrinsicElements.input: InputHTMLAttributesinput value?: Bindable<string | number> | undefinedvalue={const name: AtomHandle<string>name} EventProps<HTMLInputElement>.oninput?: ((event: Targeted<HTMLInputElement, Event>) => void) | undefinedoninput={(e: Targeted<HTMLInputElement, Event>e) => const name: AtomHandle<string>name.AtomHandle<string>.set: (next: string) => voidset(e: Targeted<HTMLInputElement, Event>e.currentTarget: HTMLInputElementcurrentTarget.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)} />
);
};
For one-shot mount work, use watch.once(). Return a function to clean up on unmount:
import { const watch: WatchFnwatch } from "ilha";
const const Boot: () => ViewBoot = () => {
const watch: WatchFnwatch.WatchFn.once: (fn: (ctx: WatchContext) => WatchOnceCleanup | undefined | Promise<WatchOnceCleanup | undefined>) => Instruction<undefined>once(() => {
const const onKey: (e: KeyboardEvent) => voidonKey = (e: KeyboardEvente: KeyboardEvent) => {
if (e: KeyboardEvente.KeyboardEvent.key: stringThe KeyboardEvent interface's **`key`** read-only property returns the value of the key pressed by the user, taking into consideration the state of modifier keys such as Shift as well as the keyboard locale and layout.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/key)key === "Escape") {
var console: ConsoleThe `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v24.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args) for more information.log("esc");
}
};
var window: Window & typeof globalThisThe **`window`** property of a Window object points to the window object itself.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/window)window.addEventListener<"keydown">(type: "keydown", listener: (this: Window, ev: KeyboardEvent) => any, options?: boolean | AddEventListenerOptions): void (+3 overloads)The **`addEventListener()`** method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)addEventListener("keydown", const onKey: (e: KeyboardEvent) => voidonKey);
return () => var window: Window & typeof globalThisThe **`window`** property of a Window object points to the window object itself.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/window)window.removeEventListener<"keydown">(type: "keydown", listener: (this: Window, ev: KeyboardEvent) => any, options?: boolean | EventListenerOptions): void (+3 overloads)The **`removeEventListener()`** method of the EventTarget interface removes an event listener previously registered with EventTarget.addEventListener() from the target. The event listener to be removed is identified using a combination of the event type, the event listener function itself, and various optional options that may affect the matching process; see Matching event listeners for removal.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener)removeEventListener("keydown", const onKey: (e: KeyboardEvent) => voidonKey);
});
return <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>ready</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>;
};
watch.once() accepts an async callback and hands you an abort signal. The signal aborts on unmount:
import { const watch: WatchFnwatch } from "ilha";
const const Feed: () => ViewFeed = () => {
const watch: WatchFnwatch.WatchFn.once: (fn: (ctx: WatchContext) => WatchOnceCleanup | undefined | Promise<WatchOnceCleanup | undefined>) => Instruction<undefined>once(async ({ signal: AbortSignalsignal }) => {
const const socket: WebSocketsocket = new var WebSocket: new (url: string | URL, protocols?: string | string[]) => WebSocketThe **`WebSocket`** object provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket)
A WebSocket client implementation.WebSocket("wss://example.com/live");
signal: AbortSignalsignal.AbortSignal.addEventListener<"abort">(type: "abort", listener: (this: AbortSignal, ev: Event) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)The **`addEventListener()`** method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)addEventListener("abort", () => const socket: WebSocketsocket.WebSocket.close(code?: number, reason?: string): voidThe **`WebSocket.close()`** method closes the WebSocket connection or connection attempt, if any. If the connection is already CLOSED, this method does nothing.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/close)close());
});
return <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>live</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>;
};
watch(source, fn) passes the same context per run. A new value aborts the previous run before fn executes — stale work never overwrites fresh results:
import { const atom: AtomFnatom, const watch: WatchFnwatch } from "ilha";
const const Search: () => ViewSearch = () => {
const const query: AtomHandle<string>query = atom<string>(init: string | Atom<string> | Effect<string, unknown, AtomRegistry> | Stream<string, unknown, AtomRegistry>, options?: AtomOptions<string> | undefined): AtomHandle<string>atom("");
watch<string>(source: Atom<string> | AtomHandle<string> | Stream<string, unknown, unknown>, fn: WatchCallback<string>): Instruction<undefined>watch(const query: AtomHandle<string>query, (q: stringq, { signal: AbortSignalsignal }) => {
void 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/search?q=${q: stringq}`, { RequestInit.signal?: AbortSignal | null | undefinedAn AbortSignal to set request's signal.signal });
});
return (
<JSX.IntrinsicElements.input: InputHTMLAttributesinput value?: Bindable<string | number> | undefinedvalue={const query: AtomHandle<string>query} EventProps<HTMLInputElement>.oninput?: ((event: Targeted<HTMLInputElement, Event>) => void) | undefinedoninput={(e: Targeted<HTMLInputElement, Event>e) => const query: AtomHandle<string>query.AtomHandle<string>.set: (next: string) => voidset(e: Targeted<HTMLInputElement, Event>e.currentTarget: HTMLInputElementcurrentTarget.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() / watch.once() 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, unknown, AtomRegistry> | Stream<number, unknown, AtomRegistry>, options?: AtomOptions<number> | undefined): AtomHandle<number>atom(0);
const const b: AtomHandle<number>b = atom<number>(init: number | Atom<number> | Effect<number, unknown, AtomRegistry> | Stream<number, unknown, AtomRegistry>, options?: AtomOptions<number> | undefined): 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 (
<JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton type?: string | undefinedtype="button" EventProps<HTMLButtonElement>.onclick?: ((event: Targeted<HTMLButtonElement, MouseEvent>) => void) | undefinedonclick={const reset: () => voidreset}>
{const a: AtomHandle<number>a}-{const b: AtomHandle<number>b}
</JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton>
);
};
Skip equal writes
Atoms notify on reference change. You pass equals to skip subscribers when the value is meaningfully the same:
import { const atom: AtomFnatom } from "ilha";
const const Profile: () => ViewProfile = () => {
const const user: AtomHandle<{
name: string;
}>
user = atom<{
name: string;
}>(init: {
name: string;
} | Atom<{
name: string;
}> | Effect<{
name: string;
}, unknown, AtomRegistry> | Stream<{
name: string;
}, unknown, AtomRegistry>, options?: AtomOptions<{
name: string;
}> | undefined): AtomHandle<{
name: string;
}>
atom({ name: stringname: "Ada" }, { AtomOptions<{ name: string; }>.equals?: AtomEquality<{
name: string;
}> | undefined
Skip subscriber notify when `equals(prev, next)` holds. Plain values only.equals: "structural" });
return <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>{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 user: AtomHandle
() => {
name: string;
}
user())}</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>;
};
user.set({ name: "Ada" }) does not rerun subscribers. Pass a comparator for custom rules:
import { const atom: AtomFnatom } from "ilha";
const const Name: () => ViewName = () => {
const const name: AtomHandle<string>name = atom<string>(init: string | Atom<string> | Effect<string, unknown, AtomRegistry> | Stream<string, unknown, AtomRegistry>, options?: AtomOptions<string> | undefined): AtomHandle<string>atom("Ada", {
AtomOptions<string>.equals?: AtomEquality<string> | undefinedSkip subscriber notify when `equals(prev, next)` holds. Plain values only.equals: (a: stringa, b: stringb) => a: stringa.String.toLowerCase(): stringConverts all the alphabetic characters in a string to lowercase.toLowerCase() === b: stringb.String.toLowerCase(): stringConverts all the alphabetic characters in a string to lowercase.toLowerCase(),
});
return <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>{const name: AtomHandle<string>name}</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>;
};
equals applies to plain values only. Like the initial value, it is fixed by the first call and never resets.
Async mutations
atom(Atom.fn(...)) runs an Effect when you call .set():
import * as import AtomAtom from "effect/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>, unknown, AtomRegistry> | Stream<AsyncResult<any, unknown>, unknown, AtomRegistry>, options?: AtomOptions<AsyncResult<any, unknown>> | undefined): AtomHandle<AsyncResult<any, unknown>>atom(
import AtomAtom.const fn: <unknown, any, string>(fn: (arg: string, get: Atom.FnContext) => Effect.Effect<any, unknown, AtomRegistry | Scope>, 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: {
readonly try: (signal: AbortSignal) => PromiseLike<any>;
readonly catch: (error: unknown) => unknown;
}) => Effect.Effect<any, unknown, never> (+1 overload)
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 (
<JSX.IntrinsicElements.form: FormHTMLAttributesform
EventProps<HTMLFormElement>.onsubmit?: ((event: Targeted<HTMLFormElement, SubmitEvent>) => void) | undefinedonsubmit={(e: Targeted<HTMLFormElement, SubmitEvent>e) => {
e: Targeted<HTMLFormElement, SubmitEvent>e.function preventDefault(): voidThe **`preventDefault()`** method of the Event interface tells the user agent that the event is being explicitly handled, so its default action, such as page scrolling, link navigation, or pasting text, should not be taken.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/preventDefault)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: Targeted<HTMLFormElement, SubmitEvent>e.currentTarget: HTMLFormElementcurrentTarget).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);
}}
>
<JSX.IntrinsicElements.input: InputHTMLAttributesinput name?: Attrname="email" type?: string | undefinedtype="email" placeholder?: Attrplaceholder="you@company.com" />
<JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton type?: string | undefinedtype="submit">Join waitlist</JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton>
</JSX.IntrinsicElements.form: FormHTMLAttributesform>
);
};
For optimistic updates, use Atom.optimisticFn.
Share fetches
resource() fetches once per key and shares the result. Concurrent mounts dedupe on one request; remounts seed from the cache:
import { const resource: <T>(key: string, fetcher: ResourceFetcher<T>, opts?: ResourceOptions) => Resource<T>Fetch-once, share-everywhere async state for `key`. Concurrent mounts
dedupe on one in-flight request; remounts seed from the shared cache
(and revalidate when `staleWhileRevalidate` holds). Call unconditionally
at the top level of a sync component, before any `await`.resource } from "ilha";
const const User: () => ViewUser = () => {
const const user: Resource<any>user = resource<any>(key: string, fetcher: ResourceFetcher<any>, opts?: ResourceOptions): Resource<any>Fetch-once, share-everywhere async state for `key`. Concurrent mounts
dedupe on one in-flight request; remounts seed from the shared cache
(and revalidate when `staleWhileRevalidate` holds). Call unconditionally
at the top level of a sync component, before any `await`.resource("user:42", (key: stringkey) =>
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/${key: stringkey}`).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())
);
if (const user: Resource<any>user.Resource<any>.loading: AtomHandle
() => boolean
loading()) return <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>Loading</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>;
return <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>{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 user: Resource<any>user.Resource<any>.data: AtomHandle
() => any
data())}</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>;
};
| Handle | Meaning |
|---|---|
data |
Last value, or undefined |
error |
Last failure, or undefined |
loading |
True while a request is in flight |
refetch() |
Fetch again; joins an in-flight request |
invalidate(key) drops the cached value and refetches it in every live component:
import { const invalidate: (key: string) => voidDrop the cached value for `key` and refetch it in live components.invalidate } from "ilha";
function invalidate(key: string): voidDrop the cached value for `key` and refetch it in live components.invalidate("user:42");
Call resource() unconditionally at the top level, before any await. The fetcher receives an abort signal that fires when a newer fetch starts or the component unmounts. On the server each render fetches fresh — the shared cache never crosses requests. await renderToString(Component) waits for the fetch; pass {user.data} in JSX to put the value in the HTML, since the component body renders once on the server.
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 <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>{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())}</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>;
};
Props vs state
State initialized from props does not reset when props change. Atoms hold data, not JSX.