useTable isReady state temporarily flips to false during reducer mutations, causing UI flashing
Description
When using the SpacetimeDB React SDK, the useTable hook returns a tuple [rows, isReady]. The isReady boolean correctly indicates when the initial subscription has been applied. However, when a client locally dispatches a reducer that mutates the subscribed table (e.g., an insert, update, or delete), the isReady state unexpectedly and transiently flips to false for a few milliseconds before returning to true.
Because standard React patterns rely on if (!isReady) return <Loading />, this momentary flip to false causes the entire rendered list/component to unmount and flash blank during every state update, creating a highly disruptive user experience.
Environment
- SpacetimeDB CLI Version:
2.0.4
- SpacetimeDB Server Version:
2.0.4
- SpacetimeDB SDK Version:
^2.0.4 (npm spacetimedb package)
- Runtime Environment: TypeScript/JavaScript Server Module
Steps to Reproduce
- Maintain an active server subscription via
useTable.
- Map the data to the UI using a standard readiness check:
import { useTable, useReducer } from 'spacetimedb/react';
import { tables, reducers } from './module_bindings';
export function StarList() {
const [stars, isReady] = useTable(tables.star);
const createStar = useReducer(reducers.createStar);
// Standard loading check
if (!isReady) return <div>Loading stars...</div>;
return (
<div>
<button onClick={() => createStar({ message: "Test" })}>Add Star</button>
{stars.map(star => <div key={star.id}>{star.message}</div>)}
</div>
);
}
- Click the "Add Star" button to dispatch the reducer.
- Observe the UI: the DOM briefly replaces the entire list with "Loading stars..." before immediately snapping back to the updated list.
Expected Behavior
The isReady boolean should represent whether the initial subscription has hydrated. Once it becomes true, a purely local mutation (like a reducer execution inserting a row) should not cause isReady to revert to false while the local cache handles the event.
The stars array successfully contains the local data during this time, so the isReady flag resetting is misleading and breaks UI persistence.
Actual Behavior
The isReady boolean is somehow tied to the internal snapshot recalculation or event loop of the connection.subscriptionBuilder(). During an ongoing transaction, it resets to false, causing React components respecting the isReady flag to unmount valid, existing rows data.
Root Cause Analysis
Inside useTable.ts, the hook relies on useSyncExternalStore combined with subscribeApplied state mapping.
When a transaction event triggers onInsert or onDelete, the SDK recalculates the computeSnapshot() and potentially causes React to evaluate subscribeApplied incorrectly during the strict-mode event batching, or setSubscribeApplied is inadvertently affected by the underlying SDK connection manager receiving an update event. This momentarily breaks the isReady output.
Current Workaround
Developers must ignore the isReady flag entirely for rendering guard clauses after the first load, and instead rely on the length of the data array. This prevents the UI from unmounting when isReady hallucinates a false state:
// Workaround: Do not use !isReady to block rendering
const [stars, isReady] = useTable(tables.star);
// Instead, rely on data length (which persists safely during the transient state)
if (stars.length === 0) {
// Only show loading if we also aren't ready
if (!isReady) return <div>Loading...</div>;
return <div>No stars found.</div>;
}
return <List data={stars} />
Suggested Fix
The internal logic in useTable.ts must decouple the isReady boolean from individual row mutation updates. isReady should strictly track the onApplied lifecycle of the subscription (acting as a one-way latch that stays true once the subscription is active), rather than fluctuating when the snapshot cache updates due to a row insert/delete.
useTableisReadystate temporarily flips tofalseduring reducer mutations, causing UI flashingDescription
When using the SpacetimeDB React SDK, the
useTablehook returns a tuple[rows, isReady]. TheisReadyboolean correctly indicates when the initial subscription has been applied. However, when a client locally dispatches a reducer that mutates the subscribed table (e.g., an insert, update, or delete), theisReadystate unexpectedly and transiently flips tofalsefor a few milliseconds before returning totrue.Because standard React patterns rely on
if (!isReady) return <Loading />, this momentary flip tofalsecauses the entire rendered list/component to unmount and flash blank during every state update, creating a highly disruptive user experience.Environment
2.0.42.0.4^2.0.4(npmspacetimedbpackage)Steps to Reproduce
useTable.Expected Behavior
The
isReadyboolean should represent whether the initial subscription has hydrated. Once it becomestrue, a purely local mutation (like a reducer execution inserting a row) should not causeisReadyto revert tofalsewhile the local cache handles the event.The
starsarray successfully contains the local data during this time, so theisReadyflag resetting is misleading and breaks UI persistence.Actual Behavior
The
isReadyboolean is somehow tied to the internal snapshot recalculation or event loop of theconnection.subscriptionBuilder(). During an ongoing transaction, it resets tofalse, causing React components respecting theisReadyflag to unmount valid, existingrowsdata.Root Cause Analysis
Inside
useTable.ts, the hook relies onuseSyncExternalStorecombined withsubscribeAppliedstate mapping.When a transaction event triggers
onInsertoronDelete, the SDK recalculates thecomputeSnapshot()and potentially causes React to evaluatesubscribeAppliedincorrectly during the strict-mode event batching, orsetSubscribeAppliedis inadvertently affected by the underlying SDK connection manager receiving an update event. This momentarily breaks theisReadyoutput.Current Workaround
Developers must ignore the
isReadyflag entirely for rendering guard clauses after the first load, and instead rely on the length of the data array. This prevents the UI from unmounting whenisReadyhallucinates afalsestate:Suggested Fix
The internal logic in
useTable.tsmust decouple theisReadyboolean from individual row mutation updates.isReadyshould strictly track theonAppliedlifecycle of the subscription (acting as a one-way latch that staystrueonce the subscription is active), rather than fluctuating when the snapshot cache updates due to a row insert/delete.