Skip to content

States

State is the type formerly named Wire, and Behavior before that — the FRP-literature term; the old names were removed in 1.0 (see Migrating to 1.0).

State<A> is a reactive value: the text of an input, the current user, a counter. It always has a value you can read, JSX can bind to it, and other values can be derived from it. (In FRP literature this is a "behavior" — a value across time — but you don't need the theory to use it.)

State or Stream?

If you can draw it on the screen, it's a State. If you can react to it, it's a Stream. The longer version, with a thermometer and a knock on the door: Stream vs State.

Creating

ts
import { state, constant, root, State } from "@continuum-js/frp";

const name = state(""); // a cell with a .set method
const pi = constant(3.14159); // never changes
const now = State.fromPoll(() => Date.now()); // read the world on sample

// stateful folds need an owner at module level:
const count = root(() => clicks.accum(0, (_e, n) => n + 1)); // fold over a stream
const latest = root(() => responses.hold(null)); // last occurrence

state is the everyday way to create state — a cell you read like any State and change with .set, like useState without re-runs. The event-based forms (accum, hold) shine when state is naturally "a history of things that happened"; they live in Streams. They are stateful, so they need an owner: inside a component the current scope owns them automatically; at module level wrap them in root(...), or the runtime throws a teaching error.

State with 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); an occurrence and the update it causes share one moment, and .on registers its process in the current scope.

Setting a value equal to the current one (by Object.is) is a no-op: no subscriber wakes, no DOM is touched. A State is a value across time — "changing" it to the same value is not a change. Pass your own comparison as the second argument (state(user, (a, b) => a.id === b.id)), or () => false to deliver every set. Note the flip side: mutating an object in place and setting the same reference is skipped — set a fresh object instead.

For read-modify-write there is update():

ts
count.update((n) => n + 1);
todos.update((list) => [...list, item]);

Unlike set(count.sample() + 1), the updater folds over the value staged by the current moment, so several updates inside one batch compose (+2, not +1) — sample() inside a batch would still show the pre-moment value (that's the hold delay working as documented). Equal results are skipped like any other set.

Deriving

Derived values are built with map and combine, and the expression structure is the dependency graph:

ts
const total = items.map((xs) => xs.reduce((s, i) => s + i.price, 0));
const label = combine(total, currency, (t, c) => `${t} ${c}`);

If a value can be computed from existing States, don't store it — derive it. combine joins any number of sources — data first, function last; deriving is glitch-free (see Transactions).

Derivations are formulas: map/combine/filter are just recipes until somebody listens (a JSX binding, .listen(), or a downstream node with listeners of its own). The first listener attaches the whole chain up to its sources; after the last one leaves, the chain goes back to sleep. A sleeping formula costs nothing — and sample() always answers correctly, warm or cold, by recomputing on the spot. Stateful derivations (hold, accum) are different: their value depends on every occurrence, so they belong to a scope — they stay attached for the scope's lifetime and, once it is disposed, freeze at their last value.

Reading

  • In JSX — place the State itself: {count}, class={cls}. That is a binding, not a snapshot.
  • From an event — use at; it has exact simultaneity semantics.
  • Outside the network (initialization, tests, integration code) — count.sample() returns the current value.
ts
const submitted = draft.at(submits);

Updates

count.updates is the Stream<A> of the State's changes — the bridge back to the event world, used by combinators like debounce(query.updates, 300). A cell delivers one coalesced occurrence per moment: several sets inside one batch mean subscribers see only the final value.

A State updates at the boundary of a moment: within the very transaction that changes it, readers still see the old value. That delay is what makes simultaneous reads well-defined — the whole story is in Transactions.


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