Skip to content

useLiveSuspenseQuery never releases over an on-demand collection whose loadSubset returns a Promise #1858

Description

@MAST1999
  • I've validated the bug against the latest version of DB packages (@tanstack/db@0.9.2, @tanstack/react-db@0.4.1)

Describe the bug

useLiveSuspenseQuery over an on-demand collection never leaves its Suspense fallback whenever loadSubset returns a Promise — which is every real asynchronous data source.

LoadSubsetFn is declared (options) => true | Promise<void>. If an implementation answers with the synchronous true, the boundary releases immediately. If it answers with a promise — even one that resolves on the next tick — the component re-suspends without limit.

To Reproduce

Runnable repro, real Chromium via @vitest/browser: https://gist.github.com/MAST1999/c2261d268a68610f18c5a6c45525b468

npm install && npx vitest run

Plain @tanstack/db. No persistence, no Electric, no orderBy, no limit. The only variable between passing and failing is whether loadSubset returns true or a Promise; both deliver the same two rows.

✓ a synchronous loadSubset releases the boundary                      54ms
× an async loadSubset resolving after   0ms   → text="loading" renders=23525
× an async loadSubset resolving after  50ms   → text="loading" renders=416
× an async loadSubset resolving after 500ms   → text="loading" renders=56
× an explicit queryKey does not change the outcome
× StrictMode is not the cause

Two things a reviewer would reasonably suspect are ruled out in the repro itself: passing an explicit stable queryKey does not help, and it reproduces with StrictMode removed.

The render count falling as the delay grows (23525 → 416 → 56) is the signature of a re-suspend cycle rather than a busy loop — each retry waits for the promise, so a slower promise fits fewer retries into the same five-second budget.

Expected behavior

An async loadSubset should suspend once and release when it resolves.

Likely mechanism

useLiveSuspenseQuery resets promiseRef/hasBeenReadyRef whenever collectionRef.current !== result.collection and throws observer.preload(). A component that suspends before it has ever mounted does not keep its hook state, so each retry constructs a fresh live query, which is loading, which throws a fresh preload promise, which resolves, which retries. The synchronous-true path never suspends at all, which is why it is the only passing case.

I have not confirmed the React-internals half of that (whether a fiber that suspends pre-mount discards its hook state), so treat the mechanism as a hypothesis and the measurements above as the report.

Additional context

This looks like the general case of two issues I filed earlier today, both of which are ways of arriving at a promise-returning loadSubset:

If this one is fixed, both of those stop being user-visible hangs and become merely redundant work. I'd suggest triaging this first.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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