Skip to content

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

  • 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 when deps change.
  • useMutation<M, K>(name) — returns { mutate, isPending, error, reset }. Identity-stable across renders (safe in useEffect dep arrays).
  • usePlasmaFile(ref) — resolves a FileRef to a { status, url } handle. Accepts null | undefined gracefully.
  • usePresence({ url, room?, token?, userInfo?, clientID? }) — manages a WebSocket presence subscription and returns { all, peers, me, connecting, error }. Auto-reconnects with exponential backoff. Wraps createWebSocketSubscription so callers don’t need to wire it into createPlasmaClient for 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; onSettled runs on both paths.
// 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} />
}

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 underlying LiveQuery (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.

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.

Under reference/generated/plasma-react/src.