Skip to content

Live queries

A live query answers itself: hand it the query you would run once, and you get the rows now and the rows again every time the database changes. This page is for React developers who have run a query and want the screen to stay current.

Reef’s board is one live query — a query that re-runs itself whenever the database changes and hands your screen new rows (glossary):

examples/reef/src/app/screens/BoardScreen.tsx:230-234
const db = useDb(slug, Reef);
const board = useLive(db, boardQuery);
const people = useLive(db, peopleQuery);
const labels = useLive(db, labelsQuery);

useLive(db, query) returns { rows, error, ticks }. rows is undefined until the first answer; error is set only when the query can never succeed (bad request, a wrong database name, no permission, memory limit); ticks counts updates (glossary) — each time the server tells this page that a newer version number exists (glossary). Render with key={row.id}.

Open the same workspace in two windows and drag a card. The other window moves it within about a second, locally and deployed, and Reef’s header pill pulses on every update (key={ticks} re-triggers the animation):

Two Reef windows stacked; cards moved in the top window appear in the same columns in the bottom one, tick counters climbing
Two windows on the same workspace; the live pill pulses on every update. · src/app/screens/BoardScreen.tsx:264-267

There is no refetch code anywhere in Reef. Nothing at the write site announces the change, and there is no cache to invalidate.

  • After your own write — including in the window that wrote.
  • After anyone else’s write. Other windows hear about the new version over their WebSocket connection (glossary) and re-run within about a second, locally and deployed. Writes always go over HTTPS; reads and updates share that connection.
  • A re-run is a whole re-run. No diffing: the query is evaluated again, permissions and limits applied exactly as for db.q.
  • Only news is emitted. Identical results are not emitted again, so a write the query does not see is not a re-render.
  • Dropped connections recover on their own — retries back off from 250 ms to 5 s and the connection reconnects in place.
  • A pinned view emits once and completes. useLive(db.asOf(t), query) has no news to deliver; rows stays.

usePull is the same idea for one record. Reef’s issue panel keeps one open on the selected issue; every emission — an edit from any window — resets the drafts:

examples/reef/src/app/components/IssueDetail.tsx:258
const extra = usePull(db, { id: issueId }, issueExtraShape).rows ?? null;

{ id: issueId } inline is fine — the subject is compared by value, not by identity.

A live query needs a WebSocket connection — that is how the server tells the page the database moved.

environmentlive queries
a browser, through Ramose.connect (or RamoseProvider)yes — this is the intended home
a Worker calling the server through Ramose.ServerBindingno — no WebSocket on that hop; db.live fails outright. See Live queries need a browser
Node or Bunonly where a global WebSocket exists, or one you pass to Ramose.connect

db.live(query) returns a stream of results, not a single value, so Effect.runPromise cannot run it — that rule is for one-shot calls like db.q. Consume it with Stream.runForEach:

watch.ts
import * as Effect from "effect/Effect";
import * as Stream from "effect/Stream";
await Effect.runPromise(
Stream.runForEach(db.live(todoQuery), (rows) =>
Effect.sync(() => console.log(rows)),
),
);

That call runs until you interrupt it, re-emitting the whole result set on every update. In React, useLive does this for you.

There is no per-query subscription state on the server. A live query is a re-run of the same read path, triggered by a version update. Bursts of small writes coalesce, because re-runs happen per update rather than per write.

useLive re-subscribes when the query value changes, so build queries at module scope, as queries.ts does. Changing values are Ramose.params — bind them as the third argument; an inline { issueId } is fine, and a params-only change does not blank rows:

examples/reef/src/app/components/IssueDetail.tsx:266
const comments = useLive(db, commentsQuery, { issueId });

db.asOf(t) inline is fine — views compare by value. Effect users who build a stream themselves can pass it directly (useLive(stream)) — see React hooks.