Async
The async utility wraps an async function into a reactive, awaitable controller. You can start, stop, and rerun async operations, and read their state, value, and errors as reactive signals.
Basic usage
import { function async<TInput = any, TOutput = undefined>(fn: MaybeReactive<(input: TInput) => Promise<TOutput>>): Async<TInput, TOutput> & ((...args: any[]) => TOutput | undefined)
Create an
Async
that is also callable as a signal: invoking it
(with no args) reads the current result, so it drops into any reactive
context that expects a zero-arg getter.
async } from "elements-kit/utilities/async";
const const fetchItems: Async<any, any> & ((...args: any[]) => any)
fetchItems = async<any, any>(fn: MaybeReactive<(input: any) => Promise<any>>): Async<any, any> & ((...args: any[]) => any)
Create an
Async
that is also callable as a signal: invoking it
(with no args) reads the current result, so it drops into any reactive
context that expects a zero-arg getter.
async(() => function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
fetch("/api/items").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((res: Response
res) => res: Response
res.Body.json(): Promise<any>
json()));
const fetchItems: Async<any, any> & ((...args: any[]) => any)
fetchItems.Async<any, any>.start(...args: [] | [input: any]): Async<any, any> & ((...args: any[]) => any)
Starts a new reactive async operation, stopping any currently active one.
start(); // begin reactive executionControl methods
Start for reactive execution, run for one-shot, stop to tear down.
op.start(); // run and track reactive dependencies β reruns when signals changeop.run(); // run once without tracking β does not rerun on signal changesop.stop(); // stop reactive reruns and run cleanup logicAsync implements Symbol.dispose, so using stops it automatically when it goes out of scope:
{ using op = async(() => fetch("/api/data").then((r) => r.json())).start(); await op; console.log(op.value);} // op.stop() called automatically hereReactive state
Async exposes the same reactive state interface as ReactivePromise:
op.state; // "pending" | "fulfilled" | "rejected"op.value; // resolved value (T | undefined)op.reason; // rejection reason (E | undefined)op.result; // value if fulfilled, reason if rejected, undefined if pendingop.pending; // true while pendingAll properties are reactive β reading them inside an effect or computed subscribes to changes.
Callable signal
An Async instance is also callable as a signal. op() returns op.result and tracks it as a reactive dependency:
import { effect } from "elements-kit/signals";
effect(() => { const result = op(); // undefined while pending, T when fulfilled, E when rejected console.log(result);});This makes Async composable with computed and templates.
Awaitable
Async implements .then, .catch, and .finally, so you can await it directly:
const op = async(() => Promise.resolve(123)).start();const value = await op; // 123Reactive reruns
Read signals inside the async function to make it re-execute when they change. Only signal reads before the first await are tracked.
import { function signal<T>(): Updater<T> & Computed<T> (+1 overload)
Creates a mutable reactive signal.
- Read: call with no arguments β returns the current value and
subscribes the active tracking context.
- Write: call with a value β updates the signal and schedules
downstream effects if the value changed.
signal } from "elements-kit/signals";import { function async<TInput = any, TOutput = undefined>(fn: MaybeReactive<(input: TInput) => Promise<TOutput>>): Async<TInput, TOutput> & ((...args: any[]) => TOutput | undefined)
Create an
Async
that is also callable as a signal: invoking it
(with no args) reads the current result, so it drops into any reactive
context that expects a zero-arg getter.
async } from "elements-kit/utilities/async";
const const id: Updater<number> & Computed<number>
id = signal<number>(initialValue: number): Updater<number> & Computed<number> (+1 overload)
Creates a mutable reactive signal.
- Read: call with no arguments β returns the current value and
subscribes the active tracking context.
- Write: call with a value β updates the signal and schedules
downstream effects if the value changed.
signal(1);
const const fetchTodo: Async<any, any> & ((...args: any[]) => any)
fetchTodo = async<any, any>(fn: MaybeReactive<(input: any) => Promise<any>>): Async<any, any> & ((...args: any[]) => any)
Create an
Async
that is also callable as a signal: invoking it
(with no args) reads the current result, so it drops into any reactive
context that expects a zero-arg getter.
async(() => function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
fetch(`https://jsonplaceholder.typicode.com/todos/${const id: () => number (+1 overload)
id()}`) // tracked .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((res: Response
res) => res: Response
res.Body.json(): Promise<any>
json()),).Async<any, any>.start(...args: [] | [input: any]): Async<any, any> & ((...args: any[]) => any)
Starts a new reactive async operation, stopping any currently active one.
start(); // re-fetches automatically when id changes
const id: (value: number) => void (+1 overload)
id(2); // triggers a new fetchrun() is untracked β signals inside the fn do not trigger re-runs. To get reactive reruns with explicit parameters, wrap it in an external effect:
import { effect, signal } from "elements-kit/signals";
const todoId = signal(1);
effect(() => { fetchTodo.run(todoId()); // re-fetches when todoId changes (tracked by outer effect)});Cleanups
Register cleanup logic inside your async function using onCleanup. It runs when stop() is called or when start() re-runs due to a signal change:
import { onCleanup } from "elements-kit/signals";
const query = async((id: number) => { const controller = new AbortController(); onCleanup(() => controller.abort()); return fetch(`/api/todos/${id}`, { signal: controller.signal }).then((r) => r.json(), );}).start();onCleanup also works inside run() β the cleanup fires when stop() is called or when the next run() replaces it.
See also
- Promise β the underlying
ComputedPromise/ReactivePromiseprimitive. - Data fetching β full recipe composing retry, online, window-focus.
- Signals β
onCleanup,untracked.