Skip to content

useLiveSuspenseQuery stays stuck on suspense fallback with on-demand queryCollection after dependency change #1418

@calcari

Description

@calcari
  • I've validated the bug against the latest version of DB packages

Describe the bug
When using useLiveSuspenseQuery with a queryCollectionOptions collection in syncMode: "on-demand", changing the subset scope (here via userId) causes the Suspense boundary to remain stuck on the fallback indefinitely.

The backend request for the new subset is executed correctly, but the Suspense boundary never resolves.

✅ useLiveSuspenseQuery + eager
❌ useLiveSuspenseQuery + on-demand
✅ useLiveQuery + on-demand

To Reproduce
Steps to reproduce the behavior:

  1. Go to repro link
  2. Click on tab n°2: useLiveSuspenseQuery (on-demand)
  3. Wait for the first user to load
  4. Click Next
  5. Observe that the fallback is shown forever

Expected behavior
After clicking Next, user 2 should appear after the simulated 1 second delay.

Minimal implementation
Define an on-demand collection

export const usersOnDemandCollection = createCollection(
  queryCollectionOptions({
    syncMode: 'on-demand',
    queryKey: ['users-on-demand'],
    queryFn: async (ctx) => {
      const options = parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions)
      const userId = options.filters.find((f) => f.field.join('.') === 'id')?.value as number
      const user = await fetchUser(userId)
      return [user]
    },
    queryClient,
    getKey: (item) => item.id,
  }),
)

Define a Suspense live query for one user

export const useUserLiveSuspenseOnDemand = (userId: number) => {
  const result = useLiveSuspenseQuery(
    (q) => q.from({ users: usersOnDemandCollection }).where(({ users }) => eq(users.id, userId)).findOne(),
    [userId],
  )

  return result
}

Simple scenario where the userId comes from useState.
(Imagine it comes from the route params /users/:userId)

function Content() {
  const [userId, setUserId] = useState<number>(1)
  const result = useUserLiveSuspenseOnDemand(userId)
  const user = result.data

  return (
    <div>
      User: {user?.id} {user?.name}
      <button type="button" onClick={() => setUserId((value) => Math.max(1, value - 1))} disabled={userId <= 1}>
        Previous
      </button>
      <button type="button" onClick={() => setUserId((value) => Math.min(10, value + 1))} disabled={userId >= 10}>
        Next
      </button>
    </div>
  )
}

function Tab2StateDbSuspenseOnDemand() {
  return (
    <Suspense fallback={<div className="panel">Suspense fallback...</div>}>
      <Content />
    </Suspense>
  )
}

Additional context
Using a dynamic queryKey such as ['users-on-demand', userId] instead of ['users-on-demand'] does not change the behavior
The workaround discussed in #1343 neither

Environment

{
  "@tanstack/db": "^0.6.0",
  "@tanstack/query-db-collection": "^1.0.31",
  "@tanstack/react-db": "^0.1.78",
  "@tanstack/react-query": "^5.95.2",
  "@tanstack/react-query-devtools": "^5.95.2",
  "react": "^19.2.4",
  "react-dom": "^19.2.4"
}

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