Skip to content
Ilha
Esc
navigateopen⌘Jpreview
On this page

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.
@categorycombinators@since4.0.0
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.
@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
((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.
@categorycombinators@since4.0.0
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(): string
Removes 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: string
The **`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 | null
The **`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: string
The **`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`.
@categoryconstructors@since4.0.0
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" ```
@see{@link promise} if the effectful computation is asynchronous and does not throw errors.@categoryconstructors@since2.0.0
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 | undefined
A string to set request's method.
method
: "POST",
RequestInit.body?: BodyInit | null | undefined
A BodyInit object or null to set request's body.
body
: var JSON: JSON
An 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.
@paramvalue A JavaScript value, usually an object or array, to be converted.@paramreplacer A function that transforms the results.@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.@throws{TypeError} If a circular reference or a BigInt value is found.
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.
@paramonfulfilled The callback to execute when the Promise is resolved.@paramonrejected The callback to execute when the Promise is rejected.@returnsA Promise for the completion of which ever callback is executed.
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) => FormData
The **`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 | null
The **`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.
@categoryguards@since4.0.0
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: JSON
An 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): any
Converts a JavaScript Object Notation (JSON) string into an object.
@paramtext A valid JSON string.@paramreviver A function that transforms the results. This function is called for each member of the object. If a member contains nested objects, the nested objects are transformed before the parent object is.@throws{SyntaxError} If `text` is not valid JSON.
parse
(var localStorage: Storage
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage)
localStorage
.Storage.getItem(key: string): string | null
The **`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: JSON
An 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.
@paramvalue A JavaScript value, usually an object or array, to be converted.@paramreplacer A function that transforms the results.@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.@throws{TypeError} If a circular reference or a BigInt value is found.
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.

Was this page helpful?