Skip to content

useTable: rapid query parameter changes cause 'Subscription not found' errors #4528

@clockwork-labs-bot

Description

@clockwork-labs-bot

Summary

useTable with dynamic query parameters (e.g. a prop that changes frequently) causes Subscription not found errors:

Error: Subscription not found: ((Identity(...), ConnectionId(...)), QuerySetId { id: 11745 })

Reproduction

A React component that subscribes to a filtered query where the filter value comes from a prop that changes rapidly (e.g. as the page scrolls):

const ClickerGame: React.FC<{ plotId: bigint }> = ({ plotId }) => {
    const [gameStates] = useTable(tables.game_clicker.where(game => game.plotId.eq(plotId)));
    // ...
};

When multiple instances of this component are mounted and plotId changes on scroll, the error spams continuously.

Root Cause

Each render where plotId changes produces a new SQL string (e.g. SELECT * FROM "ClickerGame" WHERE "ClickerGame"."plot_id" = 5 then ... = 6, ... = 7, etc.). The useEffect in useTable has querySql as a dependency, so every change triggers:

  1. Cleanup: cancel.unsubscribe() for the old QuerySetId
  2. New effect: connection.subscriptionBuilder().subscribe(newSql) → assigns a new QuerySetId

When the prop changes on every frame during a scroll, this creates dozens of subscribe/unsubscribe round-trips per second. The server processes an unsubscribe for a QuerySetId that was already removed (or races with a previous unsubscribe) and returns the error.

Proposed Fixes

Two independent improvements:

1. Resilient unsubscribe (error suppression)

unsubscribe() should treat "subscription not found" as a benign no-op rather than surfacing it as an error. The subscription is already gone, which is the desired outcome.

2. Debounce query changes in useTable

When querySql changes rapidly, useTable should debounce the subscribe/unsubscribe cycle (e.g. 100-200ms). This avoids flooding the server with subscription churn from props that change every frame.

In the meantime, the where clause also filters client-side, so while a subscription is in flight, the hook still returns correct results from the local cache.

Current Workaround

Subscribe to all rows and filter in the component:

const [allGames] = useTable(tables.game_clicker);
const gameState = allGames.find(g => g.plotId === plotId);

This avoids subscription churn entirely since the SQL does not change.

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