Skip to content

Streams ​

Stream<A> is a stream of things that happen: clicks, key presses, server responses. Each firing (an occurrence) carries a value; between firings the event simply isn't there — unlike a State, it has no "current value" to read.

You may not need Streams yet

For most UI code, state + plain callbacks (onClick={() => x.set(…)}) is all you need. Reach for Streams when the flow itself is the point: debouncing input, merging sources, capturing form state at submit, feeding requests into resource.

The one-sentence test: if you can draw it on the screen, it is a State; if you can react to it, it is a Stream. The longer version: Stream vs State.

Creating ​

ts
import { stream, never } from "@continuum-js/frp";
import { interval } from "@continuum-js/std";

const clicks = stream<MouseEvent>(); // clicks.fire() injects an occurrence
const ticks = interval(1000); // 1, 2, 3, … every second
const nothing = never<string>(); // no occurrences, ever

In JSX, onClick={clicks.fire} sends the DOM event straight into the network.

Transforming ​

ts
const ids = clicks.map((e) => (e.target as HTMLElement).id);
const lefts = clicks.filter((e) => e.button === 0);
const ones = clicks.mapTo(1);
const firstOnly = clicks.once();
const whileOpen = keys.when(isOpen); // passes only while the State is true

Combining ​

merge folds simultaneous occurrences with an explicit function — no "left wins by accident":

ts
const delta = Stream.merge(increments, decrements, (a, b) => a + b);
const either = errors.or(fallbacks); // left-biased shorthand

Capturing state ​

at reads a State at the event's moment; when filters by one:

ts
const submitted = draft.at(submits);

Becoming state ​

ts
const latest = responses.hold(initial); // State: last value
const count = clicks.accum(0, (_e, n) => n + 1); // State: fold
const totals = amounts.accumE(0, (a, s) => s + a); // Stream of fold steps

hold/accum are stateful, so they need an owner — automatic inside a component, root(() => …) at module level. They update at the moment's boundary — within the occurrence's own transaction, readers see the previous value (why that is a feature: Transactions) — and once their scope is disposed, they freeze at their last value.

State with several named transitions reads best as a cell plus reducers:

ts
const count = state(0)
  .on(inc, (n) => n + 1)
  .on(dec, (n) => n - 1)
  .on(reset, () => 0);

Each reducer takes (state, event); .on registers its process in the current scope.

Listening (the exit hatch) ​

e.listen(handler) runs the handler after the moment closes and returns an unsubscribe function. It is not registered with the ownership tree automatically — inside a component, wrap it:

ts
onCleanup(e.listen(handler));

Prefer bindings, at, and folds; reach for listen only at the edge of the network (logging, imperative APIs, IO — see perform).


Unfamiliar term? Every piece of jargon in these docs is explained in the glossary.