Async
In plain words
Loading data, showing the loading and error states, and never hitting a response race.
The FRP network is pure and synchronous: the moment closes, effects run after it. The asynchronous world enters and leaves through two hatches.
perform: IO as data
perform turns an event of requests into an event of results. The promise runs after the moment closes; its result opens a new moment. An error is not an exception but a Result branch:
import { perform, type Result } from "@continuum-js/frp";
const results = perform(saveClicks, (draft) => api.save(draft));
// Stream<Result<unknown, SaveResponse>>
const saved = results.filter((r) => r.ok);
const failed = results.filter((r) => !r.ok);Both branches are ordinary events: merge them, fold them with accum, show them via hold.
resource: a loading state machine
For the typical "load on trigger and show" there is resource from @continuum-js/std:
import { resource } from "@continuum-js/std";
import { Dynamic } from "@continuum-js/dom";
const state = resource(userId.updates, (id) => api.fetchUser(id));
// State<Async<User>>: { status: "idle" | "loading" | "ok" | "error" }
<Dynamic value={state}>
{(s) =>
s.status === "loading" ? (
<Spinner />
) : s.status === "error" ? (
<ErrorBox error={s.error} />
) : s.status === "ok" ? (
<Profile user={s.value} />
) : null
}
</Dynamic>;The key part: the response race is solved inside. Requests are stamped with sequence numbers; a response to a superseded request is ignored (last-request-wins). The classic "a slow response to an old request overwrote the fresh one" bug is unreproducible by construction — in React that is a manual cancelled flag in every useEffect.
Debounced search: composition instead of a hook
Stream combinators and resource snap together:
import { debounce } from "@continuum-js/std";
const settled = debounce(query.updates, 300); // Stream<string>
const results = resource(settled, (q) => api.search(q));No custom hook, no timers in the component: debounce is a pure operator over an event, resource is a state machine over the results.
Timers
interval(ms) from std is an event of ticks; delay(e, ms) shifts an event in time. Their timers stop when the event is dispose()d — in a component, tie that to the ownership tree explicitly:
import { interval } from "@continuum-js/std";
import { onCleanup } from "@continuum-js/dom";
const ticks = interval(1000);
onCleanup(() => ticks.dispose());For animation there is animationFrames() from dom — it registers with the owner by itself — and continuous time (integral/warp) in the core; see the animation guide.
The one-direction rule
Async code never "writes state back" on its own — it always produces an event, which the network folds with the usual tools (hold, accum, resource). If you feel like calling set inside .then(), that is probably a perform in disguise.
Unfamiliar term? Every piece of jargon in these docs is explained in the glossary.