Stores
A store is state owned by a model instance.
Use it for form fields, selected ids, counters, filters, derived render data, and small pieces of domain state that should live with the model. A store is scoped by the Unitflow registry: every app runtime or test registry gets its own state.
Create
Section titled “Create”import { Store } from "@unitflow/core";
const count = Store.make(0);
const draft = Store.make({ name: "", description: "",});The initial value belongs to the store declaration. A fresh registry starts from that value.
Read the current value inside an Effect with Store.get.
const current = yield* Store.get(count);Use Store.set when the next value is known.
yield* Store.set(count, 1);Use Store.update when the next value depends on the current value.
yield* Store.update(count, (value) => value + 1);Reset one or more stores to their declared initial values.
yield* Store.reset(count, draft);In normal model code, writes happen inside event handlers, queries, mutations, or other Effects owned by the model.
Derive
Section titled “Derive”Use .pipe(Store.map(...)) for one store.
const count = Store.make(0);
const isEven = count.pipe(Store.map((value) => value % 2 === 0));Use Store.combine when a value depends on several stores.
const firstName = Store.make("");const lastName = Store.make("");
const fullName = Store.combine( [firstName, lastName], (firstName, lastName) => `${firstName} ${lastName}`.trim(),);Derived stores are read-only. Write to the original stores.
UI Setters
Section titled “UI Setters”Event.setter(store) creates a model action that writes a store. It is useful
for input fields where the UI sends the next value directly.
import { Event, Store } from "@unitflow/core";import { useEvent, useStore } from "@unitflow/react";
const input = Store.make("");const setInput = Event.setter(input);
function TextInput() { const value = useStore(input); const onChange = useEvent(setInput);
return ( <input value={value} onChange={(event) => onChange(event.currentTarget.value)} /> );}React to Changes
Section titled “React to Changes”Use Store.changed(store) when a later store change should trigger model
logic. It creates an event that skips the current value and emits only future
changes.
import * as Effect from "effect/Effect";import { Event, Store } from "@unitflow/core";
const query = Store.make("");
// Store.changed converts a Store into an Event of future changes.yield* query.pipe( Store.changed, Event.handler((value) => Effect.log(`query changed to ${value}`), ),);This is the simple way to subscribe to store changes inside a model. Reach for
raw Store.stream(...) only when you need stream operators such as debounce,
merge, throttle, or schedules.
Awaiting Store Values
Section titled “Awaiting Store Values”Use Store.waitFor(store, predicate) when an Effect needs to block until a
store reaches a matching value.
const ready = yield* Store.waitFor(status, (value) => value === "ready");Persistence
Section titled “Persistence”Use Store.persist(...) to keep a store’s value in a KeyValueStore across
sessions — filters, drafts, UI preferences.
import * as Schema from "effect/Schema";
type LanguageFilter = "all" | "TypeScript" | "Rust";const LanguageSchema = Schema.Literals(["all", "TypeScript", "Rust"]);
const language = yield* Store.make<LanguageFilter>("all").pipe( Store.persist({ key: "language", schema: LanguageSchema }),);Match the schema to the store’s exact type: for a literal-union store use
Schema.Literals(...), not Schema.String. A wider schema compiles (stores
are covariant), but it would happily hydrate any stored string into a store
whose type promises a literal union — the schema is the only runtime guard.
Hydration is inline: by the time persist returns, the store already holds
the restored value, so anything built on it afterwards — a dependent query, a
combined store — sees the restored value from its first run. Every later
change is saved with a timestamp; an entry that fails to decode or outlives
timeToLive is a miss, leaving the initial value in place. Persistence is
best-effort: storage and codec errors are logged as warnings and never affect
the store itself.
The requirements gain KeyValueStore — the same layers as
Query persistence: layerStorage(() => localStorage) in the browser, layerMemory in tests.
How a model exposes stores and events is covered in Model.