Skip to content

Commit 2722abc

Browse files
committed
fix: stop fetching pages when an infinite query unmounts
1 parent 0b8c55c commit 2722abc

File tree

2 files changed

+64
-10
lines changed

2 files changed

+64
-10
lines changed

src/core/infiniteQueryBehavior.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export function infiniteQueryBehavior<
1717
const oldPages = context.state.data?.pages || []
1818
const oldPageParams = context.state.data?.pageParams || []
1919
let newPageParams = oldPageParams
20+
let cancelled = false
2021

2122
// Get query function
2223
const queryFn =
@@ -29,6 +30,10 @@ export function infiniteQueryBehavior<
2930
param?: unknown,
3031
previous?: boolean
3132
): Promise<unknown[]> => {
33+
if (cancelled) {
34+
return Promise.reject('Cancelled')
35+
}
36+
3237
if (typeof param === 'undefined' && !manual && pages.length) {
3338
return Promise.resolve(pages)
3439
}
@@ -38,11 +43,7 @@ export function infiniteQueryBehavior<
3843
pageParam: param,
3944
}
4045

41-
let cancelFn: undefined | (() => any)
4246
const queryFnResult = queryFn(queryFnContext)
43-
if ((queryFnResult as any).cancel) {
44-
cancelFn = (queryFnResult as any).cancel
45-
}
4647

4748
const promise = Promise.resolve(queryFnResult).then(page => {
4849
newPageParams = previous
@@ -51,15 +52,15 @@ export function infiniteQueryBehavior<
5152
return previous ? [page, ...pages] : [...pages, page]
5253
})
5354

54-
if (cancelFn) {
55+
if (isCancelable(queryFnResult)) {
5556
const promiseAsAny = promise as any
56-
promiseAsAny.cancel = cancelFn
57+
promiseAsAny.cancel = queryFnResult.cancel
5758
}
5859

5960
return promise
6061
}
6162

62-
let promise
63+
let promise: Promise<unknown[]>
6364

6465
// Fetch first page?
6566
if (!oldPages.length) {
@@ -109,9 +110,13 @@ export function infiniteQueryBehavior<
109110
pageParams: newPageParams,
110111
}))
111112

112-
if (isCancelable(promise)) {
113-
const finalPromiseAsAny = finalPromise as any
114-
finalPromiseAsAny.cancel = promise.cancel
113+
const finalPromiseAsAny = finalPromise as any
114+
115+
finalPromiseAsAny.cancel = () => {
116+
cancelled = true
117+
if (isCancelable(promise)) {
118+
promise.cancel()
119+
}
115120
}
116121

117122
return finalPromise

src/react/tests/useInfiniteQuery.test.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
QueryClient,
1616
QueryCache,
1717
} from '../..'
18+
import { CancelledError } from '../../core'
1819

1920
interface Result {
2021
items: number[]
@@ -720,6 +721,54 @@ describe('useInfiniteQuery', () => {
720721
})
721722
})
722723

724+
it('should stop fetching additional pages when the component is unmounted', async () => {
725+
const key = queryKey()
726+
const states: UseInfiniteQueryResult<number>[] = []
727+
let fetches = 0
728+
729+
function List() {
730+
const state = useInfiniteQuery(
731+
key,
732+
async ({ pageParam }) => {
733+
fetches++
734+
await sleep(50)
735+
return Number(pageParam)
736+
},
737+
{
738+
initialData: { pages: [1, 2, 3, 4], pageParams: [1, 2, 3, 4] },
739+
getNextPageParam: lastPage => lastPage + 1,
740+
}
741+
)
742+
743+
states.push(state)
744+
745+
return null
746+
}
747+
748+
function Page() {
749+
const [show, setShow] = React.useState(true)
750+
751+
React.useEffect(() => {
752+
setActTimeout(() => {
753+
setShow(false)
754+
}, 75)
755+
}, [])
756+
757+
return show ? <List /> : null
758+
}
759+
760+
renderWithClient(queryClient, <Page />)
761+
762+
await sleep(300)
763+
764+
expect(states.length).toBe(1)
765+
expect(fetches).toBe(2)
766+
expect(queryClient.getQueryState(key)).toMatchObject({
767+
status: 'error',
768+
error: expect.any(CancelledError),
769+
})
770+
})
771+
723772
it('should be able to override the cursor in the fetchNextPage callback', async () => {
724773
const key = queryKey()
725774
const states: UseInfiniteQueryResult<number>[] = []

0 commit comments

Comments
 (0)