UI Bindings
Queries, instances, and model definitions plug into the existing useModel and component from @virentia/react and @virentia/vue - there are no new hooks to learn. Use these bindings to render collections directly: the component subscribes to exactly what it shows.
A Query In A Component
The list subscribes to the query; each row subscribes to its own instance:
function TodoRow({ todo }: { todo: InstanceOf<typeof Todo> }) {
const t = useModel(todo);
if (!t) return null;
return <li>{t.title.value}</li>;
}
function ActiveTodos() {
const active = useModel(
todos.where(Todo.done.eq(false)).sort(Todo.priority.desc).take(50),
);
return (
<ul>
{active.items.map((t) => (
<TodoRow key={t.key} todo={t} />
))}
</ul>
);
}What happens:
- rebuilding the chain every render is free - plans are interned, results memoized;
- the list re-renders only when the result set changes; a row re-renders only on its own instance's writes - renaming one todo touches one row, not the list;
t.keyis the stable list key - no remounts onrebind, no glued rows on reuse.
In Vue the same calls return refs: active.value.items, t.value.title.value.
An Instance By Id
function TodoScreen({ id }: { id: string }) {
const todo = useModel(todos.get(id));
if (!todo) return <NotFound />;
return <h1>{todo.title.value}</h1>;
}get(id) follows the instance's own writes, resolves old ids after rebind - a route holding the temporary id keeps working - and yields null when the entity is missing or disposed. Nothing throws mid-render.
Screen Models
A model definition is a props → instance factory through its collection, so the existing screen-model signatures accept it directly:
const OrderView = component({ model: OrderScreen, view: OrderLayout });
// or imperatively:
const order = useModel(OrderScreen, { orderId });What happens:
- the instance is created in the provided scope - an ordinary
add; - prop changes merge into the instance (present keys win);
- unmount disposes the instance;
component.create()returns a controlled instance the view never disposes - the creator owns it.
Keeping State Across Unmounts: keep
useModel(SettingsScreen, props, { keep: true });
// component({ model: SettingsScreen, view, keep: true })keep changes one thing: unmount no longer disposes. A remount finds the instance again and merges the current props; life ends with an explicit remove or the scope.
Who the remount finds:
- no
idin props - the model's single instance in the scope (settings, the only form). Several live instances make that ambiguous - a dev error, not a silent pick; - an
idin props - the instance with that id (tabs, master-detail, the id comes from a route or document).
There is no cache/key option for models - the collection already is the cache, keep just leaves the instance in it.
Inspector
Collections show up in the inspector aggregated - entity counts, index sizes, query plans - instead of one entry per instance. A thousand list rows is a workload, not a thousand devtools scopes.
Contract
// React and Vue, same shapes; Vue returns refs
useModel(query): query; // live query view
useModel(collection.get(id)): Instance | null; // entity view
useModel(Definition, props?, { keep? }): Instance | null;
component({ model: Definition, view, keep?, mapProps? });
Component.create(props): Instance; // controlled instanceCommon Cases
Use the bindings for:
- list screens - a query built inline, rows keyed by
t.key; - detail screens -
useModel(todos.get(id))with anullfallback; - screen state that survives tab switches -
keep: true; - parent-owned dialogs and editors -
component.create().
Related
- Queries and indexes - why inline chains are free.
- Data in and out -
rebind, forwarding, stable keys. - React models and Vue models - the framework-side details.