feat: client db connection#3
Conversation
📝 WalkthroughWalkthroughAdds a local environment file and integrates SpacetimeDB into the React app: configures a WebSocket connection and provider, persists a connection token, tracks online/offline users via reactive tables, emits system messages on presence changes, and gates UI behind connection/identity state. Changes
Sequence Diagram(s)sequenceDiagram
participant App as React App
participant Provider as SpacetimeDBProvider
participant DB as SpacetimeDB Server
participant Storage as localStorage
App->>Provider: Initialize with DbConnection (host, db, token)
Provider->>DB: Open WebSocket connection (ws://localhost:3000)
DB-->>Provider: Connection established (identity, token)
Provider->>Storage: Save token
Provider-->>App: Expose hooks (identity, connected, tables, reducers)
App->>DB: Subscribe to `onlineUsers` / `offlineUsers` tables
DB-->>App: Stream presence changes (onInsert/onDelete)
App->>App: Emit system message on presence events
App->>DB: Call `sendMessage` reducer to post messages
DB-->>App: Stream new messages
App->>App: Merge, sort, render messages with senderName
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Tip You can customize the tone of the review comments and chat replies.Configure the |
Greptile SummaryThis PR wires up the client-side SpacetimeDB connection, replacing placeholder TODO comments with live data subscriptions via Key changes:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Browser
participant main.tsx
participant SpacetimeDBProvider
participant App.tsx
participant SpacetimeDB
Browser->>main.tsx: Load app
main.tsx->>main.tsx: Read TOKEN_KEY from localStorage
main.tsx->>SpacetimeDBProvider: Provide connectionBuilder
SpacetimeDBProvider->>SpacetimeDB: Connect (ws://localhost:3000, token?)
alt Connection successful
SpacetimeDB-->>SpacetimeDBProvider: onConnect(conn, identity, token)
SpacetimeDBProvider->>main.tsx: Store token in localStorage
SpacetimeDBProvider->>App.tsx: isActive=true, identity set
App.tsx->>SpacetimeDB: Subscribe to tables.message, tables.user
SpacetimeDB-->>App.tsx: Live data (messages, online/offline users)
App.tsx-->>Browser: Render chat UI
else Connection error
SpacetimeDB-->>SpacetimeDBProvider: onConnectError(ctx, err)
SpacetimeDBProvider->>main.tsx: console.log only
App.tsx-->>Browser: Stuck on "Connecting..." (no error shown)
end
Last reviewed commit: 4f627fa |
| VITE_SPACETIMEDB_HOST=ws://localhost:3000 | ||
| VITE_SPACETIMEDB_DB_NAME=mono-chat |
There was a problem hiding this comment.
.env.local committed to version control
.env.local is not listed in .gitignore, so this file is now tracked by git. While the current values (ws://localhost:3000 and mono-chat) are not sensitive, committing environment files is dangerous because:
- A developer could later add secrets (tokens, API keys, production URLs) to this file and they would be silently committed.
- It overrides other developers' local configurations when they pull the branch.
.env.local should be added to .gitignore and a .env.local.example (with placeholder values) provided instead. The .gitignore currently has no entry for .env* or .env.local.
# .gitignore (add these lines)
.env.local
.env.*.local
There was a problem hiding this comment.
.env.local is used for example if someone wants to setup app themselves, for production would be used .env
| const onConnectError = (_ctx: ErrorContext, err: Error) => { | ||
| console.log("Error connecting to SpacetimeDB:", err); | ||
| }; |
There was a problem hiding this comment.
Connection errors leave users stuck on "Connecting…" indefinitely
onConnectError only logs to the console. Because App.tsx renders <h1>Connecting...</h1> whenever !connected || !identity, a connection failure will leave the user staring at the loading screen forever with no actionable feedback.
Consider surfacing the error to the user, for example by storing the error in module-level state and rendering an error message:
const onConnectError = (_ctx: ErrorContext, err: Error) => {
console.error("Error connecting to SpacetimeDB:", err);
// TODO: set an error state and render it in the UI
};| const onConnect = (conn: DbConnection, identity: Identity, token: string) => { | ||
| localStorage.setItem(TOKEN_KEY, token); | ||
| console.log("Connected to SpacetimeDB with identity:", identity.toHexString()); | ||
| }; |
There was a problem hiding this comment.
Unused
conn parameter in onConnect
The conn parameter (a DbConnection) is accepted but never used. Prefix it with an underscore to signal intent and avoid potential linter warnings, similar to how _ctx is handled in onConnectError.
| const onConnect = (conn: DbConnection, identity: Identity, token: string) => { | |
| localStorage.setItem(TOKEN_KEY, token); | |
| console.log("Connected to SpacetimeDB with identity:", identity.toHexString()); | |
| }; | |
| const onConnect = (_conn: DbConnection, identity: Identity, token: string) => { |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| const prettyMessages: PrettyMessage[] = []; | ||
| const prettyMessages: PrettyMessage[] = messages | ||
| .concat(systemMessages) | ||
| .sort((a, b) => (a.sent.toDate() > b.sent.toDate() ? 1 : -1)) |
There was a problem hiding this comment.
Sort comparator does not handle equal timestamps
The comparator (a, b) => (a.sent.toDate() > b.sent.toDate() ? 1 : -1) returns -1 when both timestamps are equal instead of 0. While this is unlikely to cause visible bugs in practice, it is technically incorrect and may produce non-deterministic ordering for messages with identical timestamps (e.g., system messages generated at the same Timestamp.now() call).
| .sort((a, b) => (a.sent.toDate() > b.sent.toDate() ? 1 : -1)) | |
| .sort((a, b) => { | |
| const aTime = a.sent.toDate().getTime(); | |
| const bTime = b.sent.toDate().getTime(); | |
| return aTime - bTime; | |
| }) |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/App.tsx`:
- Line 77: The handlers currently call setName and setMessage even for
whitespace-only input; update the submit/update code to trim the input and only
call setName({ name: newName }) or setMessage({ message: newMessage }) when the
trimmed string is non-empty (e.g., const trimmed = newName.trim(); if (trimmed)
call setName with trimmed), apply the same guard to the setMessage call, and
ensure the stored value uses the trimmed string to avoid saving leading/trailing
whitespace.
- Around line 24-25: Extend the lifecycle hooks clientConnected and
clientDisconnected in spacetimedb to insert a system message row into the shared
"message" table (use sender: Identity.zero(), appropriate text like "User X
connected/disconnected", and any timestamp) instead of keeping systemMessages
only in local state; then update the client component (the useState for
systemMessages and newMessage and any code that pushes local systemMessages) to
subscribe to the shared messages feed (subscribe to all messages from the
"message" table) and derive system messages from that subscription so all
clients see the same event history; reference clientConnected,
clientDisconnected, Identity.zero(), and the shared "message" table when making
these changes.
- Around line 56-59: The sort comparator for prettyMessages (sorting
messages.concat(systemMessages)) currently only returns 1 or -1 and never 0;
update the comparator used in .sort(...) so it returns 0 when a.sent and b.sent
represent the same timestamp (e.g., compare numeric timestamps via
a.sent.toDate().getTime() - b.sent.toDate().getTime() or a three-way comparison)
to ensure stable and correct ordering when timestamps are equal.
In `@src/main.tsx`:
- Around line 9-12: Validate that import.meta.env.VITE_SPACETIMEDB_HOST and
import.meta.env.VITE_SPACETIMEDB_DB_NAME are present before computing TOKEN_KEY
or constructing the Spacetime DB builder: if either HOST or DB_NAME is falsy,
log a clear error and throw/exit immediately; compute TOKEN_KEY only after
validation (TOKEN_KEY = `${HOST}/${DB_NAME}/auth_token`) and apply the same
presence check before any code that constructs the builder using HOST and
DB_NAME so the app fails fast with a clear message instead of producing
undefined-based keys.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b104dd31-13ec-48e1-938a-209d35dbff1e
📒 Files selected for processing (3)
.env.localsrc/App.tsxsrc/main.tsx
| const [systemMessages, setSystemMessages] = useState([] as Types.Message[]); | ||
| const [newMessage, setNewMessage] = useState(""); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether backend lifecycle hooks exist and whether they persist connect/disconnect events.
rg -n -C3 'clientConnected|clientDisconnected|insert\(|db\.message|send_message|system'Repository: DLineone/mono-chat
Length of output: 16326
Extend lifecycle hooks to persist connect/disconnect events as shared messages.
systemMessages is local component state that resets on refresh. System messages should be emitted on the backend via the existing spacetimedb.clientConnected() and spacetimedb.clientDisconnected() lifecycle hooks—insert messages with sender: Identity.zero() into the shared message table, then subscribe to all messages on the client. This ensures all clients see the same event history.
The lifecycle hooks already exist in spacetimedb/src/index.ts (lines 68 and 81) but currently only manage the user.online field. Extend them to also insert system messages.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/App.tsx` around lines 24 - 25, Extend the lifecycle hooks clientConnected
and clientDisconnected in spacetimedb to insert a system message row into the
shared "message" table (use sender: Identity.zero(), appropriate text like "User
X connected/disconnected", and any timestamp) instead of keeping systemMessages
only in local state; then update the client component (the useState for
systemMessages and newMessage and any code that pushes local systemMessages) to
subscribe to the shared messages feed (subscribe to all messages from the
"message" table) and derive system messages from that subscription so all
clients see the same event history; reference clientConnected,
clientDisconnected, Identity.zero(), and the shared "message" table when making
these changes.
| e.preventDefault(); | ||
| setSettingName(false); | ||
| // TODO: Call `setName` reducer | ||
| setName({ name: newName }); |
There was a problem hiding this comment.
Guard against blank name/message submissions.
Both reducers are called even when input is whitespace-only, which can create empty profile names/messages.
Suggested fix
const onSubmitNewName = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
+ const trimmed = newName.trim();
+ if (!trimmed) return;
setSettingName(false);
- setName({ name: newName });
+ setName({ name: trimmed });
};
@@
const onSubmitMessage = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
+ const trimmed = newMessage.trim();
+ if (!trimmed) return;
setNewMessage("");
- sendMessage({ text: newMessage });
+ sendMessage({ text: trimmed });
};Also applies to: 83-83
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/App.tsx` at line 77, The handlers currently call setName and setMessage
even for whitespace-only input; update the submit/update code to trim the input
and only call setName({ name: newName }) or setMessage({ message: newMessage })
when the trimmed string is non-empty (e.g., const trimmed = newName.trim(); if
(trimmed) call setName with trimmed), apply the same guard to the setMessage
call, and ensure the stored value uses the trimmed string to avoid saving
leading/trailing whitespace.
| const HOST = import.meta.env.VITE_SPACETIMEDB_HOST; | ||
| const DB_NAME = import.meta.env.VITE_SPACETIMEDB_DB_NAME; | ||
| const TOKEN_KEY = `${HOST}/${DB_NAME}/auth_token`; | ||
|
|
There was a problem hiding this comment.
Fail fast when required env vars are missing.
If either env var is absent, the builder is created with invalid values and the token key becomes undefined/..., which leads to confusing connection failures.
Suggested fix
const HOST = import.meta.env.VITE_SPACETIMEDB_HOST;
const DB_NAME = import.meta.env.VITE_SPACETIMEDB_DB_NAME;
+if (!HOST || !DB_NAME) {
+ throw new Error("Missing VITE_SPACETIMEDB_HOST or VITE_SPACETIMEDB_DB_NAME");
+}
const TOKEN_KEY = `${HOST}/${DB_NAME}/auth_token`;Also applies to: 26-29
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main.tsx` around lines 9 - 12, Validate that
import.meta.env.VITE_SPACETIMEDB_HOST and
import.meta.env.VITE_SPACETIMEDB_DB_NAME are present before computing TOKEN_KEY
or constructing the Spacetime DB builder: if either HOST or DB_NAME is falsy,
log a clear error and throw/exit immediately; compute TOKEN_KEY only after
validation (TOKEN_KEY = `${HOST}/${DB_NAME}/auth_token`) and apply the same
presence check before any code that constructs the builder using HOST and
DB_NAME so the app fails fast with a clear message instead of producing
undefined-based keys.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/App.tsx (1)
82-92:⚠️ Potential issue | 🟡 MinorGuard against empty string submissions.
While
.trim()is now applied, the reducers are still called when the trimmed value is empty. This can create empty names or messages.🛡️ Suggested fix
const onSubmitNewName = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); + const trimmed = newName.trim(); + if (!trimmed) return; setSettingName(false); - setName({ name: newName.trim() }); + setName({ name: trimmed }); }; const onSubmitMessage = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); + const trimmed = newMessage.trim(); + if (!trimmed) return; setNewMessage(""); - sendMessage({ text: newMessage.trim() }); + sendMessage({ text: trimmed }); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/App.tsx` around lines 82 - 92, Prevent submitting empty strings by validating trimmed input in onSubmitNewName and onSubmitMessage: compute const trimmed = newName.trim() (or newMessage.trim()), and only call setName({ name: trimmed }) / sendMessage({ text: trimmed }) and update UI state (setSettingName(false) / setNewMessage("")) if trimmed is non-empty; otherwise return early (or keep editing state) to avoid dispatching empty values. Ensure you reference onSubmitNewName, onSubmitMessage, newName, newMessage, setName, sendMessage, setSettingName, and setNewMessage when applying the guard.
🧹 Nitpick comments (3)
src/App.tsx (3)
65-65: Prefer.toHexString()for identity comparison and cacheIdentity.zero().Per coding guidelines, identity comparisons should use
.toHexString(). Additionally,Identity.zero()is called inside the.map()callback, creating a new instance on every message iteration.♻️ Suggested refactor
Cache the zero identity outside the loop and use string comparison:
+ const zeroIdentity = Identity.zero().toHexString(); const prettyMessages: PrettyMessage[] = messages .concat(systemMessages) .sort((a, b) => a.sent.toDate().getTime() - b.sent.toDate().getTime()) .map((message) => { const user = users.find((u) => u.identity.toHexString() === message.sender.toHexString()); return { senderName: user?.name || message.sender.toHexString().substring(0, 8), text: message.text, sent: message.sent, - kind: Identity.zero().isEqual(message.sender) ? "system" : "user", + kind: zeroIdentity === message.sender.toHexString() ? "system" : "user", }; });As per coding guidelines: "Compare identities on client using
.toHexString():row.ownerId.toHexString() === myIdentity.toHexString()".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/App.tsx` at line 65, Cache Identity.zero() into a local variable outside the messages .map loop (e.g., const zeroIdentity = Identity.zero()) and change the comparison from identity objects to hex strings by using zeroIdentity.toHexString() and message.sender.toHexString() (replace kind: Identity.zero().isEqual(message.sender) ? "system" : "user" with a string comparison using the cached zeroIdentity and message.sender.toHexString()).
77-80: Consider using.toHexString()for identity comparison.For consistency with line 60 and coding guidelines, prefer string comparison over
.isEqual().♻️ Suggested refactor
const name = (() => { - const user = users.find((u) => u.identity.isEqual(identity)); + const user = users.find((u) => u.identity.toHexString() === identity.toHexString()); return user?.name || identity?.toHexString().substring(0, 8) || ""; })();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/App.tsx` around lines 77 - 80, The users.find predicate currently uses u.identity.isEqual(identity); change it to compare strings via toHexString for consistency (e.g., use u.identity.toHexString() === identity?.toHexString()), updating the anonymous IIFE that defines the name constant (the users.find call and the identity usage) so you still return user?.name || identity?.toHexString().substring(0, 8) || "" and guard against identity being undefined when calling toHexString().
126-148: Consider using stable keys instead of array indices.Using array index as
keycan cause issues if the list is reordered or items are removed. For messages, consider using a unique identifier if available (e.g., a message ID or a combination of sender + timestamp).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/App.tsx` around lines 126 - 148, The map uses the array index variable "key" as the React key in prettyMessages.map; replace it with a stable unique identifier from the message object (e.g., message.id if available, otherwise a deterministic value like the sent timestamp or a combination such as `${message.sender}-${message.sent.toDate().getTime()}`) and pass that value to the key prop on the returned element instead of "key" to avoid reordering/removal issues.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/App.tsx`:
- Around line 27-54: The useTable hook call is passing an invalid second
parameter with onInsert/onDelete callbacks; remove that object and call useTable
only with the tableRef (e.g., change useTable(tables.user.where((r) =>
r.online.eq(true)), { ... }) to useTable(tables.user.where((r) =>
r.online.eq(true))) and do the same for offlineUsers). To produce the same
system messages, add a useEffect that watches the onlineUsers array returned
from useTable and uses setSystemMessages along with Identity.zero() and
Timestamp.now() to append connect/disconnect notices when the list changes
(detect additions/removals in the effect).
---
Duplicate comments:
In `@src/App.tsx`:
- Around line 82-92: Prevent submitting empty strings by validating trimmed
input in onSubmitNewName and onSubmitMessage: compute const trimmed =
newName.trim() (or newMessage.trim()), and only call setName({ name: trimmed })
/ sendMessage({ text: trimmed }) and update UI state (setSettingName(false) /
setNewMessage("")) if trimmed is non-empty; otherwise return early (or keep
editing state) to avoid dispatching empty values. Ensure you reference
onSubmitNewName, onSubmitMessage, newName, newMessage, setName, sendMessage,
setSettingName, and setNewMessage when applying the guard.
---
Nitpick comments:
In `@src/App.tsx`:
- Line 65: Cache Identity.zero() into a local variable outside the messages .map
loop (e.g., const zeroIdentity = Identity.zero()) and change the comparison from
identity objects to hex strings by using zeroIdentity.toHexString() and
message.sender.toHexString() (replace kind:
Identity.zero().isEqual(message.sender) ? "system" : "user" with a string
comparison using the cached zeroIdentity and message.sender.toHexString()).
- Around line 77-80: The users.find predicate currently uses
u.identity.isEqual(identity); change it to compare strings via toHexString for
consistency (e.g., use u.identity.toHexString() === identity?.toHexString()),
updating the anonymous IIFE that defines the name constant (the users.find call
and the identity usage) so you still return user?.name ||
identity?.toHexString().substring(0, 8) || "" and guard against identity being
undefined when calling toHexString().
- Around line 126-148: The map uses the array index variable "key" as the React
key in prettyMessages.map; replace it with a stable unique identifier from the
message object (e.g., message.id if available, otherwise a deterministic value
like the sent timestamp or a combination such as
`${message.sender}-${message.sent.toDate().getTime()}`) and pass that value to
the key prop on the returned element instead of "key" to avoid
reordering/removal issues.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d663127c-b706-44f6-9e07-6b157a0d4b17
📒 Files selected for processing (2)
src/App.tsxsrc/main.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main.tsx
| const [onlineUsers] = useTable( | ||
| tables.user.where((r) => r.online.eq(true)), | ||
| { | ||
| onInsert: (user) => { | ||
| const name = user.name || user.identity.toHexString().substring(0, 8); | ||
| setSystemMessages((prev) => [ | ||
| ...prev, | ||
| { | ||
| sender: Identity.zero(), | ||
| text: `${name} has connected.`, | ||
| sent: Timestamp.now(), | ||
| }, | ||
| ]); | ||
| }, | ||
| onDelete: (user) => { | ||
| const name = user.name || user.identity.toHexString().substring(0, 8); | ||
| setSystemMessages((prev) => [ | ||
| ...prev, | ||
| { | ||
| sender: Identity.zero(), | ||
| text: `${name} has disconnected.`, | ||
| sent: Timestamp.now(), | ||
| }, | ||
| ]); | ||
| }, | ||
| }, | ||
| ); | ||
| const [offlineUsers] = useTable(tables.user.where((r) => r.online.eq(false))); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
spacetimedb 2.0 useTable where predicate onInsert onDelete callbacks
💡 Result:
In SpacetimeDB 2.0, there is no direct "useTable" hook in the core SDK, but the spacetimedb/react package provides React hooks like useTable for subscribing to tables with real-time updates. These hooks handle subscriptions using table references from generated module_bindings. Key features: - Subscribe to tables or filtered views using tables. from bindings. - useTable(tables.person) returns [rows, isLoading], automatically managing WebSocket subscriptions for inserts, updates, deletes. - Use .where for predicates: tables.person.where(r => r.online.eq(true)) filters rows server-side via indexed columns. - Row callbacks like onInsert, onUpdate, onDelete are available on conn.db.
in non-React code, firing on cache changes. React hooks abstract this. Example from Next.js template (client component): import { useTable, useReducer } from 'spacetimedb/react'; import { tables } from '../module_bindings'; function PersonList({ initialPeople }) { const [people, isLoading] = useTable(tables.person); // Uses server-fetched initial data until subscription applies const displayPeople = isLoading ? initialPeople : people; // ... } For filtered subscriptions: conn.subscriptionBuilder.subscribe( tables.user.where(r => r.online.eq(true)) ); This uses the query builder API with operators like eq, ne, gt, etc., on indexed columns. onInsert/onDelete callbacks are used directly on table handles for event handling: conn.db.damageEvent.onInsert((ctx, event) => { /* handle */ }); Event tables only support onInsert since rows are transient. Wrap app in SpacetimeDBProvider with connection builder. Generate bindings via spacetime generate --lang typescript. Official docs confirm React integration via spacetimedb/react subpath, with hooks like useTable and useReducer for tables and reducers.Citations:
- 1: https://spacetimedb.com/docs/2.0.0-rc1/clients/api
- 2: https://spacetimedb.com/docs/2.0.0-rc1/clients/typescript
- 3: https://spacetimedb.com/docs/clients/typescript
- 4: https://registry.npmjs.org/spacetimedb
- 5: https://spacetimedb.com/docs/2.0.0-rc1/quickstarts/react/
- 6: https://spacetimedb.com/docs/quickstarts/nextjs
- 7: https://spacetimedb.com/docs/2.0.0-rc1/quickstarts/react
- 8: https://spacetimedb.com/docs/2.0.0-rc1/quickstarts/nextjs
🌐 Web query:
spacetimedb/react useTable hook signature parameters API documentation
💡 Result:
The useTable hook from spacetimedb/react subscribes to a SpacetimeDB table and returns the rows and loading state for real-time updates in React components. Signature: useTable<DbConnection, RowType>(tableRef: TableRef): [rows: RowType[], isLoading: boolean] Parameters: - tableRef: A TableRef from generated module_bindings, e.g., tables.person or filtered like tables.user.where(r => r.online.eq(true)). Type-safe query builder for subscriptions. Usage requires wrapping the app in . Examples: const { rows: messages } = useTable<DbConnection, Message>('message'); const { rows: people, isLoading } = useTable(tables.person); const { rows: onlineUsers } = useTable('user', where(eq('online', true))); Must be used within a SpacetimeDBProvider. Automatically handles subscriptions and real-time sync. Compatible with React StrictMode.
Citations:
- 1: https://spacetimedb.com/docs/sdks/typescript/quickstart/
- 2: https://www.npmjs.com/package/spacetimedb
- 3: https://spacetimedb.com/docs/quickstarts/nextjs
- 4: Implements React Hooks for the TypeScript SDK clockworklabs/SpacetimeDB#3255
Remove invalid callback parameters from useTable hook.
The useTable hook signature in spacetimedb/react accepts only a single tableRef parameter and returns [rows, isLoading]. The onInsert and onDelete callbacks are not valid parameters for the React hook. These callbacks are available only on server-side conn.db.<table> handles, not on the client-side React hook.
The .where() predicate is correct, but remove the second parameter object:
const [onlineUsers] = useTable(tables.user.where((r) => r.online.eq(true)));
const [offlineUsers] = useTable(tables.user.where((r) => r.online.eq(false)));For real-time updates on insert/delete events, rely on the subscription mechanism within useTable itself, which automatically syncs table changes. Handle UI updates through useEffect hooks observing the onlineUsers state if needed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/App.tsx` around lines 27 - 54, The useTable hook call is passing an
invalid second parameter with onInsert/onDelete callbacks; remove that object
and call useTable only with the tableRef (e.g., change
useTable(tables.user.where((r) => r.online.eq(true)), { ... }) to
useTable(tables.user.where((r) => r.online.eq(true))) and do the same for
offlineUsers). To produce the same system messages, add a useEffect that watches
the onlineUsers array returned from useTable and uses setSystemMessages along
with Identity.zero() and Timestamp.now() to append connect/disconnect notices
when the list changes (detect additions/removals in the effect).
Summary by CodeRabbit
New Features
Chores