Skip to content

React isReady issue #4642

@lunazhaira3

Description

@lunazhaira3

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

  1. Maintain an active server subscription via useTable.
  2. 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>
    );
}
  1. Click the "Add Star" button to dispatch the reducer.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions