Skip to content

Commit a106d63

Browse files
incepterautofix-ci[bot]TkDodo
authored
fix(core): retry for infinite queries (issue #8046) (#8049)
* Add reproduction test for infinite loop retries (issue #8046) * ci: apply automated fixes * fix: retry for infinite queries The retryer lives above the fetchFn, and it re-runs the fetchFn whenever a retry happens. Usually, the fetchFn is a thin wrapper around the actual queryFn passed by the user. However, for infinite queries, it fetches all pages in a loop. The retryer breaks out of this loop if an error occurs on e.g. the second page, and then retries by running the fetchFn - which will re-set the loop This fix hoists the currentPage counter out of the fetchFn - into the closure created by onFetch. The outer closure is created from running `query.fetch` once, so it won't be re-set between retries. The fix also re-writes the fetch loop to always take the `currentPage` into account, where it was previously treating the first page differently --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Dominik Dorfmeister <[email protected]>
1 parent d29c37a commit a106d63

File tree

2 files changed

+89
-19
lines changed

2 files changed

+89
-19
lines changed

packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx

+74-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
22
import { waitFor } from '@testing-library/react'
33
import { CancelledError, InfiniteQueryObserver } from '..'
44
import { createQueryClient, queryKey, sleep } from './utils'
5-
import type { InfiniteQueryObserverResult, QueryCache, QueryClient } from '..'
5+
import type {
6+
InfiniteData,
7+
InfiniteQueryObserverResult,
8+
QueryCache,
9+
QueryClient,
10+
} from '..'
611

712
describe('InfiniteQueryBehavior', () => {
813
let queryClient: QueryClient
@@ -323,4 +328,72 @@ describe('InfiniteQueryBehavior', () => {
323328

324329
unsubscribe()
325330
})
331+
332+
test('InfiniteQueryBehavior should not enter an infinite loop when a page errors while retry is on #8046', async () => {
333+
let errorCount = 0
334+
const key = queryKey()
335+
336+
interface TestResponse {
337+
data: Array<{ id: string }>
338+
nextToken?: number
339+
}
340+
341+
const fakeData = [
342+
{ data: [{ id: 'item-1' }], nextToken: 1 },
343+
{ data: [{ id: 'item-2' }], nextToken: 2 },
344+
{ data: [{ id: 'item-3' }], nextToken: 3 },
345+
{ data: [{ id: 'item-4' }] },
346+
]
347+
348+
const fetchData = async ({ nextToken = 0 }: { nextToken?: number }) =>
349+
new Promise<TestResponse>((resolve, reject) => {
350+
setTimeout(() => {
351+
if (nextToken == 2 && errorCount < 3) {
352+
errorCount += 1
353+
reject({ statusCode: 429 })
354+
return
355+
}
356+
resolve(fakeData[nextToken] as TestResponse)
357+
}, 10)
358+
})
359+
360+
const observer = new InfiniteQueryObserver<
361+
TestResponse,
362+
Error,
363+
InfiniteData<TestResponse>,
364+
TestResponse,
365+
typeof key,
366+
number
367+
>(queryClient, {
368+
retry: 5,
369+
staleTime: 0,
370+
retryDelay: 10,
371+
372+
queryKey: key,
373+
initialPageParam: 1,
374+
getNextPageParam: (lastPage) => lastPage.nextToken,
375+
queryFn: ({ pageParam }) => fetchData({ nextToken: pageParam }),
376+
})
377+
378+
// Fetch Page 1
379+
const page1Data = await observer.fetchNextPage()
380+
expect(page1Data.data?.pageParams).toEqual([1])
381+
382+
// Fetch Page 2, as per the queryFn, this will reject 2 times then resolves
383+
const page2Data = await observer.fetchNextPage()
384+
expect(page2Data.data?.pageParams).toEqual([1, 2])
385+
386+
// Fetch Page 3
387+
const page3Data = await observer.fetchNextPage()
388+
expect(page3Data.data?.pageParams).toEqual([1, 2, 3])
389+
390+
// Now the real deal; re-fetching this query **should not** stamp into an
391+
// infinite loop where the retryer every time restarts from page 1
392+
// once it reaches the page where it errors.
393+
// For this to work, we'd need to reset the error count so we actually retry
394+
errorCount = 0
395+
const reFetchedData = await observer.refetch()
396+
397+
expect(reFetchedData.data?.pageParams).toEqual([1, 2, 3])
398+
})
326399
})

packages/query-core/src/infiniteQueryBehavior.ts

+15-18
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ export function infiniteQueryBehavior<TQueryFnData, TError, TData, TPageParam>(
1313
): QueryBehavior<TQueryFnData, TError, InfiniteData<TData, TPageParam>> {
1414
return {
1515
onFetch: (context, query) => {
16+
const options = context.options as InfiniteQueryPageParamsOptions<TData>
17+
const direction = context.fetchOptions?.meta?.fetchMore?.direction
18+
const oldPages = context.state.data?.pages || []
19+
const oldPageParams = context.state.data?.pageParams || []
20+
let result: InfiniteData<unknown> = { pages: [], pageParams: [] }
21+
let currentPage = 0
22+
1623
const fetchFn = async () => {
17-
const options = context.options as InfiniteQueryPageParamsOptions<TData>
18-
const direction = context.fetchOptions?.meta?.fetchMore?.direction
19-
const oldPages = context.state.data?.pages || []
20-
const oldPageParams = context.state.data?.pageParams || []
21-
const empty = { pages: [], pageParams: [] }
2224
let cancelled = false
23-
2425
const addSignalProperty = (object: unknown) => {
2526
Object.defineProperty(object, 'signal', {
2627
enumerable: true,
@@ -78,8 +79,6 @@ export function infiniteQueryBehavior<TQueryFnData, TError, TData, TPageParam>(
7879
}
7980
}
8081

81-
let result: InfiniteData<unknown>
82-
8382
// fetch next / previous page?
8483
if (direction && oldPages.length) {
8584
const previous = direction === 'backward'
@@ -92,22 +91,20 @@ export function infiniteQueryBehavior<TQueryFnData, TError, TData, TPageParam>(
9291

9392
result = await fetchPage(oldData, param, previous)
9493
} else {
95-
// Fetch first page
96-
result = await fetchPage(
97-
empty,
98-
oldPageParams[0] ?? options.initialPageParam,
99-
)
100-
10194
const remainingPages = pages ?? oldPages.length
10295

103-
// Fetch remaining pages
104-
for (let i = 1; i < remainingPages; i++) {
105-
const param = getNextPageParam(options, result)
96+
// Fetch all pages
97+
do {
98+
const param =
99+
currentPage === 0
100+
? (oldPageParams[0] ?? options.initialPageParam)
101+
: getNextPageParam(options, result)
106102
if (param == null) {
107103
break
108104
}
109105
result = await fetchPage(result, param)
110-
}
106+
currentPage++
107+
} while (currentPage < remainingPages)
111108
}
112109

113110
return result

0 commit comments

Comments
 (0)