@ramose/react
v0.1.1
Published
React bindings for Ramose: RamoseProvider owns one Client per subtree, useRamose hands it back, useDb memoises a Db from it. Hooks only — no UI, no styling.
Maintainers
Readme
@ramose/react
npm install @ramose/react @ramose/alchemyReact bindings for Ramose. The provider owns one Client per subtree —
connect on mount, close on unmount or when its options change — and the hooks
hand it back. Named imports, not a namespace:
import { RamoseProvider, useDb } from "@ramose/react";
<RamoseProvider url={RAMOSE_URL} token={tokenSource}>
<App />
</RamoseProvider>;
const db = useDb("todos", Todos); // inside <App />API
<RamoseProvider {...ClientOptions}>— callsRamose.connect(options), memoised onurland the identity oftoken/fetch/webSocket; closes the previous client when they change and on unmount. StrictMode's mount → close → mount re-connects, so the tree never holds a closed client.useRamose(): Client— the client the nearest provider owns. Throws outside a provider.useDb(name, catalog): Db—client.db(name, catalog), memoised on[client, name, catalog], so a stableDbreference falls out for effect and memo deps.useLive(db, query): Live/useLive(stream): Live— a standing read as state:{ rows, error, ticks }. The query form memoisesdb.live(query)on the view (structural, so an inlinedb.asOf(t)keeps one subscription pert, not per render) andqueryidentity; the stream form takes a stream built elsewhere and re-subscribes when its identity changes — no provider needed.rowsisundefineduntil the first emission and resets when the inputs change;erroris the terminal failure only (completion of a pinned view keeps the lastrows);tickscounts emissions after the first.useQuery(db, query): Async<R>— one-shotdb.q(query). Re-runs when the view changes (structural, so an inlinedb.asOf(t)re-runs pert, not per render) orqueryidentity changes.loading: trueover the previousdatais the in-flight state — no flash toundefinedon scrub — and stale results are dropped last-write-wins by issue order.usePull(db, subject, pattern): Live<Pull | null>— standingdb.livePull.subjectis compared structurally ({ id }or a lookup ref written inline is fine);null(entity retracted) is an emission, not an end; over a pinned view the stream emits once and completes.useBasis(db): number | undefined—db.basis()on mount, re-read on every session wake (a tick, a local write, a reconnect); onasOf(t)views answerston the first render with no request.useTransact(options?): Transact— one hook for running writes (any Effect withR = never, really) from event handlers:// src/todos.ts — the write is a generator over the tx builder export const addTodo = (db: TodosDb, title: string) => db.transact(function* (tx) { const t = yield* tx.entity(); yield* t.add(Todo.title, title); }); // in the component const tx = useTransact({ onError: (e) => toast(errorMessage(e)) }); <button disabled={tx.pending} onClick={() => void tx.run(addTodo(db, title))} />;runresolves to theExitinstead of throwing, so handlers stayvoid-safe;pendingis true while any run is in flight;errorholds the last-settled failure's error (not the cause) for inline rendering, clears when a run settles successfully or onclearError(), andonErrorfires per failure. Concurrent runs settle independently — the last settler winserror, whatever order the runs started in. It takes nodbargument — it runs whatever Effect the caller built, so it composes with a module-singletonDbjust as well as withuseDb, and works without a provider. An effect settling after unmount touches no state, butonErrorstill fires: the toast host usually outlives the form that ran the write.errorMessage(error): string—e.message ?? e._tag ?? String(e), the one-liner every toast wants. EveryDbErrorcarries amessage, so a policy denial (Unauthorized) toasts its server-written message; bare tagged errors fall back to the tag.
Two rules the memo imposes
tokenmust be stable. Build theTokenSourceonce —Ramose.token.jwt(mint)at module scope, or in auseMemo— and pass that. An Effect built inline in the render changes identity every render, and the provider re-connects every render.- Multi-tenant remount is React's
key.<RamoseProvider key={tenant} url={…}>closes the old tenant's client and connects the new one whentenantchanges.
One rule the hooks impose
Queries and pull patterns are compared by identity — hoist them (they
are stable values), or the run / subscription re-keys every render. The
db argument and usePull's subject are the exceptions: both are
compared structurally across useLive / useQuery / usePull /
useBasis, so db.asOf(t) and { id: 17 } written inline are fine.
