a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1# Async Effects
2
3Volt’s `asyncEffect` helper runs asynchronous workflows whenever one or more signals change.
4It handles abort signals, debounce/throttle scheduling, retries, and cleanup so you can focus on data fetching logic instead of wiring.
5
6## When to use it
7
8- Fetching or mutating remote data in response to signal changes.
9- Performing background work that should cancel when inputs flip rapidly.
10- Retrying transient failures without duplicating boilerplate.
11- Triggering imperative side effects (e.g., analytics) that return cleanups.
12
13## Basic Example
14
15In this example, if you change `query` with `query.set("new value")` the effect re-runs.
16
17```ts
18import { asyncEffect, signal } from "voltx.js";
19
20const query = signal("");
21const results = signal([]);
22
23asyncEffect(async () => {
24 if (!query.get()) {
25 results.set([]);
26 return;
27 }
28
29 const response = await fetch(`/api/search?q=${encodeURIComponent(query.get())}`);
30 results.set(await response.json());
31}, [query]);
32```
33
34If the effect returns a cleanup function it is invoked before the next execution and on disposal.
35
36## Abortable Fetches
37
38Pass `{ abortable: true }` to receive an `AbortSignal`.
39VoltX aborts the previous run each time dependencies change or when you dispose the effect.
40
41```ts
42asyncEffect(
43 async (signal) => {
44 const response = await fetch(`/api/files/${fileId.get()}`, { signal });
45 data.set(await response.json());
46 },
47 [fileId],
48 { abortable: true },
49);
50```
51
52## Debounce and Throttle
53
54- `debounce: number` waits until inputs are quiet for the specified milliseconds.
55- `throttle: number` skips executions until the interval has elapsed; the latest change runs once the window closes.
56
57```ts
58asyncEffect(
59 async () => {
60 await saveDraft(documentId.get(), draftBody.get());
61 },
62 [draftBody],
63 { debounce: 500 },
64);
65```
66
67Combine `debounce` and `abortable` to cancel in-flight saves when the user keeps typing.
68
69## Retry Strategies
70
71`retries` controls how many times VoltX should re-run the effect after it throws.
72`retryDelay` adds a pause between attempts. Use `onError` for custom logging or to expose a manual `retry()` hook.
73
74```ts
75asyncEffect(
76 async () => {
77 const res = await fetch("/api/profile");
78 if (!res.ok) throw new Error("Request failed");
79 profile.set(await res.json());
80 },
81 [refreshToken],
82 {
83 retries: 3,
84 retryDelay: 1000,
85 onError(error, retry) {
86 toast.error(error.message);
87 retry(); // optionally kick off another attempt immediately
88 },
89 },
90);
91```
92
93## Cleanup and disposal
94
95Hold on to the disposer returned by `asyncEffect` when you need to stop reacting:
96
97```ts
98const stop = asyncEffect(async () => {
99 const subscription = await openStream();
100 return () => subscription.close();
101}, [channel]);
102
103window.addEventListener("beforeunload", stop);
104```
105
106VoltX automatically runs the cleanup when dependencies change, when the effect retries successfully, and when you call the disposer.
107
108## Tips
109
110- Keep the dependency list stable & wrap derived values in computeds if necessary.
111- Throw errors from the effect body to trigger retries or the `onError` callback.
112- Prefer `debounce` for text inputs and `throttle` for scroll/resize signals.
113- Always check abort signals before committing expensive results when `abortable` is enabled.