-
Notifications
You must be signed in to change notification settings - Fork 167
fix(db): useLiveInfiniteQuery pagination with async on-demand loadSubset #1209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(db): useLiveInfiniteQuery pagination with async on-demand loadSubset #1209
Conversation
This test documents a bug where useLiveInfiniteQuery doesn't request
pageSize+1 items from loadSubset for hasNextPage peek-ahead detection.
The bug causes hasNextPage to always return false when using on-demand
sync mode with Electric collections, because:
1. useLiveInfiniteQuery calls setWindow({ limit: pageSize + 1 }) in useEffect
2. But subscribeToOrderedChanges calls requestLimitedSnapshot BEFORE the
useEffect runs, using the original compiled limit (pageSize)
3. The loadSubset function receives limit=pageSize instead of limit=pageSize+1
4. This prevents the peek-ahead strategy from working correctly
Related: Discord bug report about useLiveInfiniteQuery + Electric on-demand
https://claude.ai/code/session_01FskX2noxNAj1zCiALFQXnC
🦋 Changeset detectedLatest commit: 4357559 The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
More templates
@tanstack/angular-db
@tanstack/db
@tanstack/db-ivm
@tanstack/electric-db-collection
@tanstack/offline-transactions
@tanstack/powersync-db-collection
@tanstack/query-db-collection
@tanstack/react-db
@tanstack/rxdb-db-collection
@tanstack/solid-db
@tanstack/svelte-db
@tanstack/trailbase-db-collection
@tanstack/vue-db
commit: |
|
Size Change: +10 B (+0.01%) Total Size: 92 kB
ℹ️ View Unchanged
|
|
Size Change: 0 B Total Size: 3.7 kB ℹ️ View Unchanged
|
The initial query was using `.limit(pageSize)` but `setWindow` expects `pageSize + 1` for peek-ahead detection. This caused a race condition where the first `requestLimitedSnapshot` was called with `limit = pageSize` before `setWindow` could adjust it to `pageSize + 1`. The fix uses `pageSize + 1` from the start so the compiled query includes the peek-ahead limit, ensuring `loadSubset` receives the correct limit for `hasNextPage` detection. https://claude.ai/code/session_01FskX2noxNAj1zCiALFQXnC
- Fix unused parameter lint warnings (allPages -> _allPages) - Simplify test logic using .find() instead of .filter()[0] - Condense redundant comments Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
samwillis
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this looks good, I believe the bug was that the current tests don't trigger a pushdown to load more data from the server. So while we are calling fetchNextPage to paginate, and are checking the behaviour of hasNextPage, it's not checked against on-demand collections.
I would suggest one thing, the new test is checking for a specific internal detail (we query for offset+), but really the tests should be asserting that an on demand collection as the source with incremental sync does work with useLiveInfiniteQuery. Maybe ask Claude to add those tests.
Replaces the previous implementation-detail test with a proper e2e test that verifies the actual behavior of useLiveInfiniteQuery with on-demand collections: - Initial page loads correctly with hasNextPage=true - fetchNextPage() actually loads more data via loadSubset - Multiple pages can be fetched with correct items - hasNextPage correctly reflects when no more data exists This test catches bugs where the incremental sync doesn't properly fetch data from the backend when paginating. https://claude.ai/code/session_01FskX2noxNAj1zCiALFQXnC
This test reproduces a bug where useLiveInfiniteQuery doesn't fetch subsequent pages when loadSubset returns a Promise (async mode). Root cause identified in collection-subscriber.ts: - When loadSubset returns a Promise, pendingOrderedLoadPromise is set - loadMoreIfNeeded returns early while the promise is pending - When the promise resolves, pendingOrderedLoadPromise is cleared - BUT loadMoreIfNeeded is NOT re-triggered to check if more data is needed This affects Electric on-demand mode where all data comes from async loadSubset calls. The initial page loads correctly, but fetchNextPage fails to trigger additional loadSubset calls. https://claude.ai/code/session_01FskX2noxNAj1zCiALFQXnC
When useLiveInfiniteQuery uses an on-demand collection with async loadSubset, the second page was never loaded because: 1. When setWindow() was called for the next page, maybeRunGraph's callback was never called because the graph had no pending work This fix ensures the graph run callback is called at least once even when there's no pending work, so setWindow() can trigger loadMoreIfNeeded for lazy loading scenarios. https://claude.ai/code/session_01FskX2noxNAj1zCiALFQXnC
62e4bc3 to
2186630
Compare
- Extract createOnDemandCollection helper to reduce test duplication - Extend async on-demand test to verify all 3 pages and hasNextPage=false - Add peek-ahead boundary test for pageSize+1 items - Replace silent catch block with explicit re-throw in test helper - Add @tanstack/db to changeset (independently versioned) - Remove redundant comments and tighten callback-guarantee comment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
🎉 This PR has been released! Thank you for your contribution! |
Summary
Fixes
useLiveInfiniteQuerynot paginating when used with on-demand collections that have asyncloadSubset(like Electric). Two independent bugs combined to makehasNextPagealwaysfalseand subsequent pages never load.Root Cause
Bug 1 — Peek-ahead never worked on page 1:
hasNextPageis determined by requestingpageSize + 1items and checking if the extra item exists. But the initial query compiled with.limit(pageSize), so the peek-ahead item could never be returned.hasNextPagewas provably alwaysfalseon the first page for query-function-based infinite queries.Bug 2 — Async loadSubset never triggered for page 2+: When
fetchNextPage()callssetWindow()to increase the limit,loadMoreIfNeededmust run to trigger a newloadSubsetcall. ButloadMoreIfNeededis invoked via a callback insidemaybeRunGraph, which only fires during graph processing steps. If the graph has no pending work (common after an asyncloadSubsetcompletes), the callback never fires and pagination stalls.Approach
Fix 1 (
useLiveInfiniteQuery.ts): Change initial query from.limit(pageSize)to.limit(pageSize + 1)to match the peek-ahead contract already used bysetWindow()on subsequent pages.Fix 2 (
collection-config-builder.ts): After the graph processing loop, ensure the callback fires at least once even whenpendingWork()is false. This letsloadMoreIfNeededrun aftersetWindow()increases the limit or after an asyncloadSubsetresolves.Key Invariants
setWindow()calls requestpageSize + 1items (peek-ahead)loadMoreIfNeededis always invoked after limit changes, regardless of graph statecallbackCalled) prevents double-invocation when the graph does have workNon-goals
useLiveInfiniteQuery(filed as bug: useLiveInfiniteQuery Promise handling can leave isFetchingNextPage stuck #1240)getNextPageParamconfig (filed as cleanup: getNextPageParam is required but never used in useLiveInfiniteQuery #1241)useEffectcleanup for stale Promise handlers (tracked in bug: useLiveInfiniteQuery Promise handling can leave isFetchingNextPage stuck #1240)Verification
pnpm --filter react-db testAll 85 tests pass (30 in
useLiveInfiniteQuery.test.tsx), including:loadSubsetreceivespageSize + 1)pageSize + 1items →hasNextPagetrue, data excludes extra item)hasNextPagetransitions)loadSubset, full lifecycle)isFetchingNextPagelifecycle with async loadingFiles Changed
packages/react-db/src/useLiveInfiniteQuery.tspageSize + 1for peek-aheadpackages/db/src/query/live/collection-config-builder.tspackages/react-db/tests/useLiveInfiniteQuery.test.tsx.changeset/fix-infinite-query-peek-ahead.md@tanstack/dband@tanstack/react-dbFixes #1206