-
Notifications
You must be signed in to change notification settings - Fork 163
Description
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 completesWith .orderBy() but no .limit():
isLoadingisfalseon first renderdatais[]- Component renders "no items yet" instead of "loading..."
- Data appears after the API fetch completes (next render)
With .orderBy().limit(1000):
isLoadingistrueon first render- Component correctly renders "loading..."
isLoadingbecomesfalseanddatapopulates 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:
-
Empty snapshot: In
change-events.ts,getOrderedKeyshitssortedKeys.slice(0, NaN)which returns[]. So even if the source collection has data, the snapshot delivers nothing. -
Broken loading state tracking: The
NaNpropagates intoloadSubsetoptions, which breaksisLoadingSubsettracking. WithoutisLoadingSubsetbeing set totrue,updateLiveQueryStatuscallsmarkReady()immediately — transitioning the collection toreadystatus before the API fetch completes. This is whyisLoadingisfalsewith empty data.
Expected behavior
A query with .orderBy() but no .limit() on an on-demand collection should:
- Report
isLoading: trueuntil the API fetch completes - Populate
dataafter 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