You're viewing the v2.3 docs.
Ledger docs
Guides

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:

typescript
// 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);
MethodDescription
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.

typescript
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:

typescript
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.

typescript
// 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:

  1. Looks at which collections the change touched.
  2. Re-evaluates only the queries reading those collections.
  3. Compares each new result to the last one by shallow identity.
  4. 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.

Esc
↑↓ navigate ↵ open Esc close