Model Kinds
model and staticModel declare fields, traits, and relations the same way, live in the same collections, and expose the same instance surface. They differ in one thing: when the units from setup are created. This page is about that difference, and about when neither is the right tool and a plain core owner is.
staticModel: Units Once
A static model builds its reactive graph a single time. An instance is a lightweight scope over the shared units - creating one writes field values, nothing else:
const Todo = staticModel({
data: { done: f.boolean(false) },
setup(self) {
const toggled = event<void>();
reaction({
on: toggled,
run: () => {
self.done.value = !self.done.value;
},
});
return { toggled };
},
});setup runs once, at declaration. Every reaction still behaves per instance - await a.toggled() flips only a - and the instance API on self (self.id, self.json()) is available inside unit bodies, where a concrete instance exists. Disposed instance scopes are pooled and reused.
Per-instance external resources still have a home: self.onCleanup from a unit body ties an unsubscribe to the ambient instance - no store, no per-instance units.
This is the default kind: a thousand list rows, feed entries, or table cells per frame is a normal load.
model: Units Per Instance
A dynamic model runs setup for every instance, and setup receives the creation input as its second argument:
const ReportScreen = model({
data: {
period: f.string("month"),
rows: f.array(f.number(), []).local(),
},
setup(self, props) {
// this closure belongs to ONE instance - create whatever it needs
const refresh = event<void>();
const total = computed(() => self.rows.value.reduce((a, b) => a + b, 0));
reaction({
on: refresh,
run: async () => {
self.rows.value = await api.report(self.period.value, props.filters);
},
});
return { refresh, total };
},
});
const report = collection(ReportScreen).add({ period: "week", filters: { team: 1 } });
await report.refresh();
report.total.value;What happens:
- stores, events, computeds, and reactions are created for this instance and owned by it -
dispose()tears them all down; propsis the rawaddinput - keys that are not fields (likefiltersabove) are visible tosetupeven though they are not stored;- because each instance owns real units,
setupmay branch: create different reactions per instance, capture per-instance closures, hold resources.
The price is real allocations per instance. Use model for few, individual entities: screens, editors, wizards, long-lived workspaces.
Choosing Between Them
staticModel | model | |
|---|---|---|
| Units | one shared graph | created in setup per instance |
| Instance cost | a scope and a value map | stores + reactions + an owner |
setup runs | once, at declaration | on every add |
setup arguments | (self) | (self, props) - the creation input |
| Per-instance closures | no - state lives in fields | yes |
| Pooling | disposed scopes are reused | no - disposal is a real teardown |
| Typical count | hundreds to thousands | a handful |
Everything else - fields, traits, relations, queries, serialization, keep in bindings - is identical. Start with staticModel; switch to model when an instance genuinely needs its own dynamically created units.
And Plain owner?
The core owner is the primitive underneath: a disposable tree of units and resources. A dynamic model instance is an owner inside - model adds the entity layer on top:
- a declaration: fields that validate input and serialize back;
- a home: the collection, with get-or-create, id lookup, and merge semantics;
- relations with delete policies, queries with indexes,
rebindand aliases.
Rule of thumb:
- a domain entity - something with data, an id, a place in lists - is a
modelorstaticModel; - an app subsystem - a socket manager, a player, a cache, a background process - is an
owner: it has resources and lifetime, but no wire shape, no id, and nothing to query; - if you find yourself giving an
owneran id registry and a serializer, it wanted to be amodel.
Contract
model({
with?, data?, name?,
setup?(self, props): members | void, // per instance; props is the add input
});
staticModel({
with?, data?, name?,
setup?(self): members | void, // once; instance API in unit bodies only
});Both return a definition usable with collection, traits, relations, unions, and bindings interchangeably.
Common Cases
- List rows, feed items, table cells -
staticModel. - Screens and editors with per-instance wiring -
model, often withkeep. - Domain entities shared by both worlds - either kind; the surface is the same, so switching later is a one-word change.
- Non-entity subsystems - core
owner, not a model at all.
Related
- Collections and instances - the shared instance surface.
- Owners and cleanup - the primitive underneath.
- UI bindings - screen models and
keep.