Skip to Content
React Package

React Package

@kalamdb/react adds React hooks and render-prop components for KalamDB live rows. Use it when a React app needs realtime tables, typed mutations, or a screen with many live datasets.

Install

BASH
npm i @kalamdb/react @kalamdb/client react react-domnpm i @kalamdb/orm drizzle-orm

@kalamdb/orm and drizzle-orm are needed for typed table mode. Raw SQL mode only needs @kalamdb/react and @kalamdb/client.

What It Adds

ExportTypeUse it for
KalamProvidercomponentPut one KalamDB client in React context.
LiveQuerycomponentRender one live query with typed rows or raw SQL rows.
LiveQueriescomponentRender several named live queries together.
useKalamClienthookRead the client from context, or pass one directly.
useLiveQueryhookRead one live query and call insert, update, remove, or refetch.
useLiveQuerieshookRead several Drizzle live queries with one aggregate state.
useLiveSelectionhookDerive view data from live rows.
useMutationStatehookTrack local insert, update, delete, and error state.
useMutationActionshookUse mutation helpers without opening a live query.

The live query context includes rows, state, insert, update, remove, clearError, and refetch. state.updating and state.deleting are Set<string | number> values keyed by row id.

App Setup

Create one client and wrap your app.

TSX
import { Auth, createClient } from '@kalamdb/client';import { KalamProvider } from '@kalamdb/react';import { MessagesPane } from './MessagesPane'; const client = createClient({  url: 'http://localhost:2900',  authProvider: async () => Auth.basic('admin', 'AdminPass123!'),}); export function App() {  return (    <KalamProvider client={client}>      <MessagesPane roomId="main" />    </KalamProvider>  );}

Typed Table Schema

Use kTable.user(...) when you want typed rows and typed mutation payloads.

TS
import { boolean, integer, text, timestamp } from 'drizzle-orm/pg-core';import { kTable } from '@kalamdb/orm'; export const messages = kTable.user('chat.messages', {  id: text('id').primaryKey(),  roomId: text('room_id').notNull(),  body: text('body').notNull(),  authorName: text('author_name'),  createdAt: timestamp('created_at'),}); export const counters = kTable.user('chat.counters', {  id: text('id').primaryKey(),  value: integer('value').notNull(),  isFavorite: boolean('is_favorite').notNull(),});

One Live Query

LiveQuery is the simplest UI component. The child receives rows, connection state, mutation state, and mutation helpers.

TSX
import { LiveQuery } from '@kalamdb/react';import { asc, eq } from 'drizzle-orm';import { messages } from './schema'; export function MessagesPane({ roomId }: { roomId: string }) {  return (    <LiveQuery      table={messages}      where={(table) => eq(table.roomId, roomId)}      orderBy={(table) => asc(table.createdAt)}      limit={100}      deps={[roomId]}    >      {({ rows, state, insert, update, remove, clearError }) => (        <section>          {state.error ? (            <button type="button" onClick={clearError}>{state.error.message}</button>          ) : null}           {rows.map((row) => (            <article key={row.id}>              <span>{row.body}</span>              <button                type="button"                disabled={state.updating.has(row.id)}                onClick={() => update(messages, row.id).set({ body: `${row.body}!` })}              >                edit              </button>              <button                type="button"                disabled={state.deleting.has(row.id)}                onClick={() => remove(messages, row.id)}              >                delete              </button>            </article>          ))}           <button            type="button"            disabled={state.inserting}            onClick={() => insert(messages).values({              id: crypto.randomUUID(),              roomId,              body: 'Hello from KalamDB',              createdAt: new Date(),            })}          >            send          </button>        </section>      )}    </LiveQuery>  );}

Use deps when a query depends on props or state. The live controller restarts when those values change.

Hook Form

useLiveQuery returns the same context without a render-prop wrapper.

TSX
import { useLiveQuery } from '@kalamdb/react';import { eq } from 'drizzle-orm';import { messages } from './schema'; export function MessageCount({ roomId }: { roomId: string }) {  const { rows, state, refetch } = useLiveQuery({    table: messages,    where: (table) => eq(table.roomId, roomId),    deps: [roomId],  });   return (    <button type="button" onClick={() => void refetch()}>      {state.loading ? 'Loading' : `${rows.length} messages`}    </button>  );}

Use select when a component only needs derived data.

TSX
const summary = useLiveQuery({  table: messages,  where: (table) => eq(table.roomId, roomId),  deps: [roomId],  select: (context) => ({    total: context.rows.length,    first: context.rows[0]?.body ?? '',    loading: context.state.loading,  }),});

Raw SQL Mode

Raw SQL mode is useful for simple views that do not use Drizzle tables. Rows are RowData, so convert cell values before rendering.

TSX
import { LiveQuery } from '@kalamdb/react'; export function SqlMessagesPane({ roomId }: { roomId: string }) {  const query = `SELECT id, body FROM chat.messages WHERE room_id = '${roomId}' ORDER BY created_at ASC LIMIT 100`;   return (    <LiveQuery query={query} getKey="id">      {({ rows, state, insert }) => (        <section>          {state.error ? <p>{state.error.message}</p> : null}          {rows.map((row) => (            <article key={row.id.asString()}>{row.body.asString()}</article>          ))}          <button            type="button"            disabled={state.inserting}            onClick={() => insert('chat.messages', {              id: crypto.randomUUID(),              room_id: roomId,              body: 'Hello from SQL mode',              created_at: new Date().toISOString(),            })}          >            send          </button>        </section>      )}    </LiveQuery>  );}

Use getKey when the row key is not obvious, or when the query returns a computed key.

Many Live Queries

LiveQueries and useLiveQueries open one controller per named query. The aggregate state is useful for dashboards and assistant screens.

TSX
import { LiveQueries } from '@kalamdb/react';import { asc, eq } from 'drizzle-orm';import { counters, messages } from './schema'; export function Dashboard({ roomId }: { roomId: string }) {  return (    <LiveQueries      queries={{        messages: {          table: messages,          where: (table) => eq(table.roomId, roomId),          orderBy: (table) => asc(table.createdAt),          deps: [roomId],        },        counters: {          table: counters,          orderBy: (table) => asc(table.id),        },      }}    >      {(context) => (        <section>          <p>{context.state.connected ? 'connected' : 'connecting'}</p>          <p>{context.messages.rows.length} messages</p>          <p>{context.counters.rows.length} counters</p>        </section>      )}    </LiveQueries>  );}

Assistant Screen Pattern

The newest package example uses named datasets for messages, tool calls, tool results, typing, presence, and approvals.

TSX
import { useLiveQueries, useLiveSelection } from '@kalamdb/react';import { desc, eq } from 'drizzle-orm';import { approvals, messages, presence, toolCalls, toolResults, typing } from './schema'; export function AssistantWorkspace({ threadId }: { threadId: string }) {  const live = useLiveQueries({    queries: {      messages: { table: messages, where: (table) => eq(table.threadId, threadId), deps: [threadId] },      toolCalls: {        table: toolCalls,        where: (table) => eq(table.threadId, threadId),        orderBy: (table) => desc(table.createdAt),        deps: [threadId],      },      toolResults: {        table: toolResults,        where: (table) => eq(table.threadId, threadId),        orderBy: (table) => desc(table.createdAt),        deps: [threadId],      },      typing: { table: typing, where: (table) => eq(table.threadId, threadId), deps: [threadId] },      presence: { table: presence, where: (table) => eq(table.threadId, threadId), deps: [threadId] },      approvals: { table: approvals, where: (table) => eq(table.threadId, threadId), deps: [threadId] },    },    deps: [threadId],  });   const view = useLiveSelection(live, (context) => ({    messages: context.messages.rows,    activeToolCalls: context.toolCalls.rows.filter((row) => row.status !== 'completed'),    latestToolResults: context.toolResults.rows,    typingUsers: context.typing.rows.map((row) => row.userName),    onlineUsers: context.presence.rows.filter((row) => row.status === 'online'),    pendingApprovals: context.approvals.rows.filter((row) => row.status === 'pending'),    approve: (id: string) => context.update(approvals, id).set({ status: 'approved' }),    reject: (id: string) => context.update(approvals, id).set({ status: 'rejected' }),  }));   return (    <section>      {view.messages.map((message) => <article key={message.id}>{message.body}</article>)}      {view.typingUsers.length > 0 ? <p>{view.typingUsers.join(', ')} typing</p> : null}      {view.activeToolCalls.map((call) => <p key={call.id}>{call.status}</p>)}      {view.pendingApprovals.map((approval) => (        <button type="button" key={approval.id} onClick={() => view.approve(String(approval.id))}>          approve        </button>      ))}    </section>  );}

Mutation Helpers

Mutations work with typed tables or string table names.

TSX
await insert(messages).values({  id: crypto.randomUUID(),  roomId: 'main',  body: 'hello',  createdAt: new Date(),}); await update(messages, messageId).set({ body: 'edited' });await remove(messages, messageId); await insert('chat.messages', {  id: crypto.randomUUID(),  room_id: 'main',  body: 'raw sql payload',});

For typed tables, camel-case keys map to database column names and Drizzle encoders run before the payload is sent.

Examples

Package Examples

The React package includes small source examples in link/sdks/typescript/react/example:

  • messages-pane.tsx: typed LiveQuery with insert state.
  • sql-messages-pane.tsx: raw SQL LiveQuery with getKey.
  • assistant-workspace.tsx: useLiveQueries plus useLiveSelection for messages, tools, results, typing, presence, and approvals.

React AI Chat App

The main runnable example is examples/react-ai-chat. It shows KalamProvider, LiveQueries, conversation navigation, message history, file attachments, streamed typing tokens, final assistant inserts, and approval actions.

BASH
cd examples/react-ai-chatnpm installnpm run setupnpm run agentnpm run dev

Open the Vite URL, usually http://127.0.0.1:5176.

For browser-only demo mode, skip npm run setup and set this in .env.local:

BASH
VITE_KALAMDB_DEMO_MODE=true

Validate the example:

BASH
npm run buildnpm test

Read next: TypeScript Examples and Drizzle ORM & Generator.

Last updated on