Skip to content

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.

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.

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.

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)}
/>
);
}

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.

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");

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.