Skip to content

useLiveQuery with orderBy but no limit has incorrect isLoading on on-demand collections — transitions to ready immediately with empty data #1217

@flybayer

Description

@flybayer

Describe the bug

On fresh page load, a useLiveQuery with .orderBy() but no .limit() against an on-demand sync collection reports isLoading: false and data: [] immediately, instead of isLoading: true while the API fetch is in progress. This causes components to render empty-state UI ("no items yet") instead of loading UI.

Adding .limit(<any number>) to the query fixes the loading state behavior.

To Reproduce

const pipelineRunsStore = createCollection(
  queryCollectionOptions({
    syncMode: "on-demand",
    queryFn: async (ctx) => { /* fetches from API */ },
    getKey: (item) => item.id,
    // ...
  }),
)

// Bug: isLoading is immediately false, data is [], on fresh page load
const { data, isLoading } = useLiveQuery(
  (q) =>
    q
      .from({ run: pipelineRunsStore })
      .where(({ run }) => eq(run.pipelineId, pipelineId))
      .orderBy(({ run }) => run.createdAt, "desc"),
  [pipelineId],
)

// Workaround: adding .limit(1000) makes isLoading correctly stay true until fetch completes

With .orderBy() but no .limit():

  • isLoading is false on first render
  • data is []
  • Component renders "no items yet" instead of "loading..."
  • Data appears after the API fetch completes (next render)

With .orderBy().limit(1000):

  • isLoading is true on first render
  • Component correctly renders "loading..."
  • isLoading becomes false and data populates after the API fetch completes

Root Cause

In collection-subscriber.ts, subscribeToOrderedChanges computes:

const { orderBy, offset, limit, index } = orderByInfo
// When no .limit() is used: offset = 0, limit = undefined

subscription.requestSnapshot({
  orderBy: normalizedOrderBy,
  limit: offset + limit,  // 0 + undefined = NaN
})

The NaN limit propagates through two paths:

  1. Empty snapshot: In change-events.ts, getOrderedKeys hits sortedKeys.slice(0, NaN) which returns []. So even if the source collection has data, the snapshot delivers nothing.

  2. Broken loading state tracking: The NaN propagates into loadSubset options, which breaks isLoadingSubset tracking. Without isLoadingSubset being set to true, updateLiveQueryStatus calls markReady() immediately — transitioning the collection to ready status before the API fetch completes. This is why isLoading is false with empty data.

Expected behavior

A query with .orderBy() but no .limit() on an on-demand collection should:

  • Report isLoading: true until the API fetch completes
  • Populate data after the fetch resolves

AI Suggested fix

Guard against undefined limit in subscribeToOrderedChanges:

// Before
subscription.requestSnapshot({
  orderBy: normalizedOrderBy,
  limit: offset + limit,
})

// After
subscription.requestSnapshot({
  orderBy: normalizedOrderBy,
  limit: limit !== undefined ? offset + limit : undefined,
})

Same for the requestLimitedSnapshot call above it.

Workaround

Add an explicit .limit() to any query that uses .orderBy() on an on-demand collection:

useLiveQuery(
  (q) => q.from({ run: store }).where(...).orderBy(...).limit(1000),
  [dep],
)

Versions

  • @tanstack/db: 0.5.21
  • @tanstack/react-db: 0.1.65
  • @tanstack/query-db-collection: 1.0.18

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