From b8b3650ddccb45e82631c83b6df5bd3c9d0a0ea7 Mon Sep 17 00:00:00 2001 From: abu-tahir-0 Date: Tue, 10 Nov 2026 07:52:15 +0530 Subject: [PATCH 1/2] fix(query-core): refetch cached infinite query pages without getNextPageParam When pages were already cached, the refetch path called getNextPageParam for page 1+, which is optional for calls that do not pass `pages` and threw a TypeError. Reuse the cached pageParams when the callback is absent; when it is provided, behavior is unchanged. --- .changeset/tidy-pages-refetch.md | 5 ++ .../src/__tests__/queryClient.test.tsx | 59 +++++++++++++++++++ .../query-core/src/infiniteQueryBehavior.ts | 4 +- 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 .changeset/tidy-pages-refetch.md diff --git a/.changeset/tidy-pages-refetch.md b/.changeset/tidy-pages-refetch.md new file mode 100644 index 00000000000..8d0e87aa28a --- /dev/null +++ b/.changeset/tidy-pages-refetch.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-core': patch +--- + +Reuse cached `pageParams` when refetching an infinite query that has no `getNextPageParam`, instead of throwing `TypeError: options.getNextPageParam is not a function`. diff --git a/packages/query-core/src/__tests__/queryClient.test.tsx b/packages/query-core/src/__tests__/queryClient.test.tsx index 13134872d14..9425c1a6703 100644 --- a/packages/query-core/src/__tests__/queryClient.test.tsx +++ b/packages/query-core/src/__tests__/queryClient.test.tsx @@ -1685,6 +1685,36 @@ describe('queryClient', () => { }) expect(result).toStrictEqual([20]) }) + + it('should refetch all cached pages using their pageParams when getNextPageParam is not provided', async () => { + const key = createKey() + + queryClient.setQueryData(key, { + pages: ['cached page 0', 'cached page 1'], + pageParams: [0, 1], + }) + + const queryFn = vi.fn< + (context: { pageParam: number }) => Promise + >(({ pageParam }: { pageParam: number }) => + Promise.resolve(`page ${String(pageParam)}`), + ) + + const result = await queryClient.infiniteQuery({ + queryKey: key, + queryFn, + initialPageParam: 0, + }) + + expect(result).toStrictEqual({ + pages: ['page 0', 'page 1'], + pageParams: [0, 1], + }) + expect(queryFn).toHaveBeenCalledTimes(2) + expect( + queryFn.mock.calls.map(([context]) => context.pageParam), + ).toStrictEqual([0, 1]) + }) }) /** @deprecated */ @@ -1779,6 +1809,35 @@ describe('queryClient', () => { // this check ensures we're exiting the fetch loop early expect(count).toBe(3) }) + + it('should refetch cached pages using their pageParams when getNextPageParam is not provided', async () => { + const key = createKey() + + queryClient.setQueryData(key, { + pages: ['cached page 0', 'cached page 1'], + pageParams: [0, 1], + }) + + const queryFn = vi.fn< + (context: { pageParam: number }) => Promise + >(({ pageParam }: { pageParam: number }) => + Promise.resolve(`page ${String(pageParam)}`), + ) + + // oxlint-disable-next-line tanstack-query/no-restricted-syntax -- grandfathered direct test + await queryClient.prefetchInfiniteQuery({ + queryKey: key, + queryFn, + initialPageParam: 0, + }) + + expect(queryClient.getQueryState(key)?.status).toBe('success') + expect(queryClient.getQueryData(key)).toStrictEqual({ + pages: ['page 0', 'page 1'], + pageParams: [0, 1], + }) + expect(queryFn).toHaveBeenCalledTimes(2) + }) }) describe('infiniteQuery used for prefetching', () => { diff --git a/packages/query-core/src/infiniteQueryBehavior.ts b/packages/query-core/src/infiniteQueryBehavior.ts index 20bc95ff225..ca7cfcdd0b2 100644 --- a/packages/query-core/src/infiniteQueryBehavior.ts +++ b/packages/query-core/src/infiniteQueryBehavior.ts @@ -112,7 +112,9 @@ export function infiniteQueryBehavior( ? oldPageParams[0] === undefined ? options.initialPageParam : oldPageParams[0] - : getNextPageParam(options, result) + : typeof options.getNextPageParam === 'function' + ? getNextPageParam(options, result) + : oldPageParams[currentPage] if (currentPage > 0 && param == null) { break } From 42b4f5006a07d2283a9e972465c90490388d0b1a Mon Sep 17 00:00:00 2001 From: Abu Tahir Date: Tue, 10 Nov 2026 09:10:07 +0530 Subject: [PATCH 2/2] fix(query-core): report no next page without getNextPageParam --- .../__tests__/infiniteQueryObserver.test.tsx | 18 ++++++++++++++++++ .../query-core/src/infiniteQueryBehavior.ts | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/query-core/src/__tests__/infiniteQueryObserver.test.tsx b/packages/query-core/src/__tests__/infiniteQueryObserver.test.tsx index 4c0ca35fdf4..916e2909ea1 100644 --- a/packages/query-core/src/__tests__/infiniteQueryObserver.test.tsx +++ b/packages/query-core/src/__tests__/infiniteQueryObserver.test.tsx @@ -174,6 +174,24 @@ describe('InfiniteQueryObserver', () => { expect(observer.getCurrentResult().hasNextPage).toBe(false) }) + it('should report no next page when getNextPageParam is not provided', () => { + const key = createKey() + + queryClient.setQueryData(key, { + pages: ['page 0', 'page 1'], + pageParams: [0, 1], + }) + + // @ts-expect-error getNextPageParam is optional when no pages are requested + const observer = new InfiniteQueryObserver(queryClient, { + queryKey: key, + queryFn: () => sleep(10).then(() => 'page'), + initialPageParam: 0, + }) + + expect(observer.getCurrentResult().hasNextPage).toBe(false) + }) + it('should stop refetching if null is returned from getNextPageParam', async () => { const key = createKey() let next: number | null = 2 diff --git a/packages/query-core/src/infiniteQueryBehavior.ts b/packages/query-core/src/infiniteQueryBehavior.ts index ca7cfcdd0b2..f7c74892d03 100644 --- a/packages/query-core/src/infiniteQueryBehavior.ts +++ b/packages/query-core/src/infiniteQueryBehavior.ts @@ -193,7 +193,7 @@ export function hasNextPage( options: InfiniteQueryPageParamsOptions, data?: InfiniteData, ): boolean { - if (!data) return false + if (!data || !options.getNextPageParam) return false return getNextPageParam(options, data) != null }