React Live Queries
@kalamdb/react turns a KalamDB live query into React rows and state. Start with useLiveQuery: the hook owns subscription setup and cleanup, while your component renders loading, empty, error, and data states.
Prepare the live-query example
Create the app.live_messages table from the TypeScript inbox setup, then install:
These examples target a React TypeScript app. In Next.js, put the provider and query components behind a 'use client' boundary. Pass the URL and your auth integration’s token getter from the application; never bundle an admin password.
One client per signed-in session
The auth integration must return a current token, including after refresh. Keep the client stable across ordinary renders. When the signed-in account changes, unmount its query tree, disconnect its client, and create a client for the new session. KalamProvider supplies context; it does not own disconnecting your client.
Define the typed table
Use the schema generator in an existing project, or copy this definition for the example table. Declaring a TypeScript table does not create the server table.
Render rows and handle writes
Open the screen twice with the same user. Send a message in one, edit it in the other, then delete it. Both lists should follow the server changes. These helpers send writes; the live subscription supplies the resulting rows. The example does not add an optimistic local row.
Understand state, errors, and retries
| Value | How to use it |
|---|---|
rows | Current materialized rows; replace the rendered list rather than merging whole snapshots yourself |
state.loading | Show startup/reload progress; an empty array during loading is not an empty result |
state.status | Controller state: idle, loading, live, reconnecting, error, or disposed |
state.error | Latest mutation error, otherwise the live-controller error |
state.inserting | Disable a repeated send while a write is pending |
state.updating / state.deleting | Sets of row keys for pending mutations |
clearError() | Clear the mutation error only; it does not repair a failed subscription |
refetch() | Restart the live subscription; call after fixing a table, query, or permission problem |
A transient connection interruption is different from invalid SQL, missing tables, or denied access. Let the SDK manage transport reconnects. Fix permanent causes before using Reload. Do not blindly retry writes: a lost response does not prove the server rejected the write.
state.connected and state.status describe the live controller, not a complete transport-health monitor. The current hook does not forward every socket disconnect into its snapshot. Use the client’s connection callbacks for an application-wide offline indicator; do not treat state.connected as proof of freshness. Retained rows may remain visible while a reload is underway.
To test error handling, temporarily change the table name to a missing table. The screen should show an error, not “No messages yet.” Restore the schema and reload/remount. Test a rejected write with an identity that lacks write access: keep the input, show the error, and avoid an unhandled promise rejection. See authentication for credential refresh.
Filters, ordering, and cleanup
Put captured filter values in deps. Use Drizzle eq(table.room, roomId) instead of interpolating untrusted input into SQL. The hook cleans up its old subscription when its query changes and on unmount.
When switching rooms, render <MessagesPane key={roomId} roomId={roomId} /> to reset the component’s draft and old rows immediately. For an account switch, replace the session client too.
orderBy sorts locally and limit selects displayed rows; the matching row set remains retained so a deletion can reveal the next row. This is not a server-side memory bound. Narrow where for large datasets. The underlying subscription uses the supported SELECT ... FROM ... WHERE ... shape.
Do not disconnect the shared client in every list component. The application/session owner is responsible for it.
Raw SQL and render-prop alternatives
For fixed SQL without Drizzle models, use LiveQuery. Raw rows use typed cell accessors:
LiveQuery also accepts the typed table, where, orderBy, limit, and deps options from the hook. Raw SQL mode does not expose a params option; prefer typed predicates for dynamic values.
Multiple queries and derived views
useLiveQueries and LiveQueries create one controller per named dataset. Inspect a dataset’s own error when one panel fails; the aggregate state alone cannot explain which query failed.
Use select on useLiveQuery to derive one view from its context. useMutationActions(client) provides mutations without opening a query; useMutationState exposes the mutation-state reducer for custom integrations. Neither adds an offline outbox.