-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Description
Describe the bug
Escalating this discussion to an issue: #10078
Although each page of an infinite query is not considered a separate query in the query cache - I think it is design flaw that useInfiniteQuery does not paginate when the cached data is restored (it dumps everything into the data prop, rather than waiting for a fetchNextPage call, which should instantaneously provide the next page of data - almost like a simulated fetch).
On a conceptual level... yes, one infinite query that hits the same endpoint for the same data with the same parameters (but different pagination props) seems reasonable to store as one query, rather than multiple queries. Or does it?
While that's debatable, what I think is not is that useInfiniteQuery should consistently handle pagination. Loaded data ≠ loaded/visible rows. One might say: "just manually paginate the data once it's loaded" and sure, there are ways to hack this - but would require rebuilding hasNextPage and fetchNextPage, which would become redundant on first load because useInfiniteQuery does this for us.
So I think the solution to producing a consistent API (and/or solving this problem as-is) is to either remove pagination by eliminating useInfiniteQuery and adapt useQuery to iterate and fetch multiple pages (renders useInfiniteQuery useless - but at least the queries would be paginated consistently... not really the best option), or bake the same functionality into rendering/providing cached data to the data prop in a way that respects the pagination utils - creating a "distribution" proxy layer between the in-memory cache and the data prop to do so.
I hope that makes sense, and that my direction is reasonable. The current approach of "dump all pages in at once, and expect the client to re-implement the pagination props exposed by the useInfiniteQuery hook" feels out of line with the purpose of the hook.
For an explicit "one page at a time" UI where you have to manually select the page, I'm sure this would work fine, because you can just select the page from InfiniteData.
BUT for an infinite-scroll setup that relies on hasNextPage and fetchNextPage to render "loading" rows and only display a page worth of rows - this isn't optimal, because it just displays 100+ rows at once instead of the page size (~25), creating an inconsistent UI experience.
Again, the only way to fix this is to either hack the query cache somehow or re-implement pagination the same way useInfiniteQuery does when it makes network requests.
Here's some code to give an example:
export function useThoughtsQueryOptions(props?: {
authToken: string | null
filter?: ThoughtsFilterOptions | null
}) {
const { authToken = null, filter } = props ?? {}
return useMemo(
() =>
router.thoughts.get.infiniteOptions({
input: (pageParam: CursorDefinition | null) => ({
pagination: {
type: "cursor",
cursors: pageParam ? [pageParam] : null,
limit: config.listPaginationLimit
},
filter
}),
context: { authToken },
initialPageParam: null,
getNextPageParam: lastPage => {
if (!lastPage.hasMore) return null
const thoughts = lastPage.thoughts
if (!(thoughts && thoughts.length)) return null
const lastThoughtIndex = thoughts.length - 1
const lastThought = thoughts[lastThoughtIndex]
if (!lastThought) return null
const cursorDef: CursorDefinition = {
field: "created-at",
value: lastThought.createdAt
}
return cursorDef
}
}),
[authToken, filter]
)
}
const getThoughtsQueryOptions = useThoughtsQueryOptions({
authToken,
filter
})
const { isFetching, data, error, hasNextPage, fetchNextPage, refetch } =
useInfiniteQuery(getThoughtsQueryOptions)
const thoughts = useMemo(
() => data?.pages.flatMap(page => page.thoughts ?? []) ?? null,
[data?.pages]
)
const pagination: RaycastPaginationOptions = useMemo(
() => ({
pageSize: config.listPaginationLimit,
hasMore: hasNextPage,
onLoadMore: fetchNextPage
}),
[hasNextPage, fetchNextPage]
)The result is perfect pagination when thoughts are loaded over the network - but when loaded from a persisted cache in combination with experimental_createQueryPersister, all of the thoughts appear at once.
Another note: an unfortunate side effect of this is that instead of stale "pages" being reloaded per-page that's viewed/loaded, it reloads ALL 100+ pages even if they're not within the viewport (re: page size).
On second thought, this may somewhat be related to the integration of the persister... but not really, because the persisterFn that we pass to defaultOptions.queries.persister is a standard API of the Query Client, therefore the responsibility for the issue most likely lies on the useInfiniteQuery side, and how it consumes paginated query cache data.
To conclude, I think developing a "faux-loading page distribution proxy" layer between the query cache and the data prop is the solution here.
Your minimal, reproducible example
N/A, can create if absolutely needed. Note: all Tanstack Query pagination examples do not build, throwing error - "Error: turbo.createProject is not supported by the wasm bindings."
Steps to reproduce
See description
Expected behavior
See description
How often does this bug happen?
Every time
Screenshots or Videos
Not needed for understanding
Platform
- MacOS: 26.0.1
- Raycast: 1.104.4
- Tanstack Query: 1.13.4
Tanstack Query adapter
react-query
TanStack Query version
1.13.4
TypeScript version
5.9.3
Additional context
No response