Reactive queries
Queries are the heart of Ledger. They're plain, type-safe descriptions of the data you want — and when you make them live, they keep your UI in sync without a single manual refresh.
The query builder
Start from a collection and chain refinements. Each method returns a new query, so queries are immutable and safe to share:
// Every query starts from a collection and chains refinements.
const overdue = db.tasks
.where((t) => t.due < Date.now() && !t.done)
.sort('due', 'asc')
.limit(20);
// A query is lazy — nothing runs until you read it…
const rows = overdue.all(); // snapshot, runs now
// …or subscribe to it:
const handle = overdue.live().subscribe(render); | Method | Description |
|---|---|
where(fn) | Keep records where the predicate returns true. |
sort(key, dir?) | Order by a field or a comparator. Chain for tie-breakers. |
limit(n) | Cap the number of results. |
offset(n) | Skip the first n results — handy for pagination. |
all() | Run once and return a snapshot array. |
live() | Return a subscribable that re-emits on change. |
Live subscriptions
Call .live() and .subscribe() to receive the result now and on every
relevant change. subscribe returns an unsubscribe function.
const activeCount = db.tasks.where((t) => !t.done).live();
// The callback runs immediately with the current result,
// then again on every change that affects it.
activeCount.subscribe((tasks) => {
badge.textContent = String(tasks.length);
});
// Mutations anywhere trigger the right subscribers:
db.tasks.update('t-42', { done: true }); // badge updates Prefer the framework bindings (useQuery) in components — they call subscribe on mount and clean up automatically.
Sorting and limiting
Stack sort calls for multi-key ordering, or pass a comparator for full control:
db.tasks
.where((t) => t.project === 'docs')
.sort('priority', 'desc') // primary sort
.sort('title', 'asc') // tie-breaker
.limit(10)
.offset(0);
// Sort by a computed value with a comparator:
db.tasks.sort((a, b) => score(b) - score(a)); Derived queries
Because queries are values, you can build one from another. Ledger evaluates the shared parent once and fans the result out to each child — no duplicated work.
// Compose queries by deriving one from another.
const mine = db.tasks.where((t) => t.assignee === currentUser.id);
const myOpen = mine.where((t) => !t.done).live();
const myDone = mine.where((t) => t.done).live();
// 'mine' is evaluated once and shared by both derived queries. How updates stay cheap
When you mutate a record, Ledger doesn't blindly re-run every subscription. Instead it:
- Looks at which collections the change touched.
- Re-evaluates only the queries reading those collections.
- Compares each new result to the last one by shallow identity.
- Notifies a subscriber only if its result actually changed.
A query over 10,000 records that no longer matches a mutated row costs a single predicate check — not a full re-scan. This is what keeps large local datasets snappy.