@sh1n4ps/plasma-react
@sh1n4ps/plasma-react gives you the four React primitives most apps need:
one provider component and three hooks. Everything else — the
client instance, the query builder, the mutators — comes from
@sh1n4ps/plasma-client and @sh1n4ps/plasma-core.
What’s here
Section titled “What’s here”PlasmaProvider— makes the client available to hooks below it in the tree. Constructed once at your app’s root.usePlasma()— pulls the current client out of context. Rare to use directly; the other hooks call it internally.useLiveQuery(factory, deps)— subscribes to a live query and returns the current snapshot. Re-runs whendepschange.useMutation<M, K>(name)— returns{ mutate, isPending, error, reset }. Identity-stable across renders (safe inuseEffectdep arrays).usePlasmaFile(ref)— resolves aFileRefto a{ status, url }handle. Acceptsnull | undefinedgracefully.usePresence({ url, room?, token?, userInfo?, clientID? })— manages a WebSocket presence subscription and returns{ all, peers, me, connecting, error }. Auto-reconnects with exponential backoff. WrapscreateWebSocketSubscriptionso callers don’t need to wire it intocreatePlasmaClientfor presence-only use cases. See Presence.useSyncStatus()— returns a reactive{ online, outboxDepth, lastError, syncing }snapshot for the current client. Use for status bars, “N unsaved changes” badges, offline banners.useMutation(name, { onSuccess?, onError?, onSettled? })— the mutation hook now takes a second-arg options bag with lifecycle callbacks. Callbacks fire after the local optimistic apply either resolves or throws;onSettledruns on both paths.
Key patterns
Section titled “Key patterns”// The provider — construct the client outside the render tree.const plasma = createPlasmaClient({ ... })plasma.start()
<PlasmaProvider client={plasma}> <App /></PlasmaProvider>
// A live-list component.function TodoList() { const rows = useLiveQuery( () => plasma.db.select().from(todos).where(eq(todos.done, 0)), [], ) return <ul>{rows.map((t) => <li key={t.id}>{t.title}</li>)}</ul>}
// A mutation button with pending state.function AddTodoButton() { const create = useMutation<typeof mutators, "createTodo">("createTodo") return ( <button disabled={create.isPending} onClick={() => create.mutate({ id: plasma.newId(), title: "hi", updatedAt: Date.now() })} > Add </button> )}
// A thumbnail from a file() column.function Thumb({ ref }: { ref: FileRef | null | undefined }) { const handle = usePlasmaFile(ref) if (handle?.status !== "ready" && handle?.status !== "local") return null return <img src={handle.url} />}Behaviour notes / gotchas
Section titled “Behaviour notes / gotchas”Things that are hard to spot from signatures alone:
useLiveQuery returns [] on the initial render
Section titled “useLiveQuery returns [] on the initial render”The reactive engine sets up asynchronously — the first render
always returns [], and the first live delivery comes in on the
next tick. If you need to distinguish “still loading” from “no
results”, you’ll want either:
- an explicit loading state driven off a first-mount ref, or
- a
whenReady()await on the underlyingLiveQuery(dropping to the imperative API — see Live queries)
rows.length === 0 alone doesn’t tell you which state you’re in.
useLiveQuery(factory, deps): the factory must be pure
Section titled “useLiveQuery(factory, deps): the factory must be pure”The factory is re-run whenever deps change, and the returned
builder is diffed against the previous mount to decide whether to
re-subscribe. Side effects inside the factory will fire on every
re-eval; keep it a pure query construction.
useMutation.mutate rejects on error
Section titled “useMutation.mutate rejects on error”mutate() returns a Promise<void> that rejects if the
mutator body throws locally. It’s not error-only-via-state:
const create = useMutation<typeof mutators, "createTodo">("createTodo")
// ❌ Unhandled rejection if the mutator throws:<button onClick={() => create.mutate({ ... })}>Add</button>
// ✅ Attach a catch, or await the returned Promise:<button onClick={() => create.mutate({ ... }).catch((e) => console.warn(e))}>Add</button>The error state is populated too, so you can drive UI from that
— but the Promise contract is: reject on throw. Callers that
don’t await or .catch() will trigger unhandled-rejection
handlers.
useMutation.mutate resolves at the optimistic-apply boundary
Section titled “useMutation.mutate resolves at the optimistic-apply boundary”The returned Promise resolves as soon as the local IDB apply
succeeds, not after the server push/pull cycle completes. If
you close a dialog in onSubmit, the dialog closes instantly —
that’s the intent. Reversion (on server-side reject) happens on a
later rebase; wire it through client.onError if you need to
surface it.
Full symbol index
Section titled “Full symbol index”Under reference/generated/plasma-react/src.
Where to go from here
Section titled “Where to go from here”- Quick Start — assembly of all four hooks
- Live queries (Guide) — subscribeDelta, whenReady, IVM eligibility
- Files and blobs (Guide) — usePlasmaFile’s full FileHandle union