Repository: tannerlinsley/react-query
Pull request #1761 (2021-02-07 10:31:20)

fix: stop fetching pages when an infinite query unmounts
Fixes https://github.com/tannerlinsley/react-query/issues/1412

vercel[bot]: This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).  
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/tannerlinsley/react-query/2ez2u2hx7](https://vercel.com/tannerlinsley/react-query/2ez2u2hx7)  
✅ Preview: *In Progress*



Patch:
diff --git a/src/core/infiniteQueryBehavior.ts b/src/core/infiniteQueryBehavior.ts
index 6bca69785..beed03d62 100644
--- a/src/core/infiniteQueryBehavior.ts
+++ b/src/core/infiniteQueryBehavior.ts
@@ -17,6 +17,7 @@ export function infiniteQueryBehavior<
         const oldPages = context.state.data?.pages || []
         const oldPageParams = context.state.data?.pageParams || []
         let newPageParams = oldPageParams
+        let cancelled = false
 
         // Get query function
         const queryFn =
@@ -29,6 +30,10 @@ export function infiniteQueryBehavior<
           param?: unknown,
           previous?: boolean
         ): Promise<unknown[]> => {
+          if (cancelled) {
+            return Promise.reject('Cancelled')
+          }
+
           if (typeof param === 'undefined' && !manual && pages.length) {
             return Promise.resolve(pages)
           }
@@ -38,11 +43,7 @@ export function infiniteQueryBehavior<
             pageParam: param,
           }
 
-          let cancelFn: undefined | (() => any)
           const queryFnResult = queryFn(queryFnContext)
-          if ((queryFnResult as any).cancel) {
-            cancelFn = (queryFnResult as any).cancel
-          }
 
           const promise = Promise.resolve(queryFnResult).then(page => {
             newPageParams = previous
@@ -51,15 +52,15 @@ export function infiniteQueryBehavior<
             return previous ? [page, ...pages] : [...pages, page]
           })
 
-          if (cancelFn) {
+          if (isCancelable(queryFnResult)) {
             const promiseAsAny = promise as any
-            promiseAsAny.cancel = cancelFn
+            promiseAsAny.cancel = queryFnResult.cancel
           }
 
           return promise
         }
 
-        let promise
+        let promise: Promise<unknown[]>
 
         // Fetch first page?
         if (!oldPages.length) {
@@ -109,9 +110,13 @@ export function infiniteQueryBehavior<
           pageParams: newPageParams,
         }))
 
-        if (isCancelable(promise)) {
-          const finalPromiseAsAny = finalPromise as any
-          finalPromiseAsAny.cancel = promise.cancel
+        const finalPromiseAsAny = finalPromise as any
+
+        finalPromiseAsAny.cancel = () => {
+          cancelled = true
+          if (isCancelable(promise)) {
+            promise.cancel()
+          }
         }
 
         return finalPromise


Test patch:
diff --git a/src/hydration/tests/ssr.test.tsx b/src/hydration/tests/ssr.test.tsx
index aff7e8642..4a764d59b 100644
--- a/src/hydration/tests/ssr.test.tsx
+++ b/src/hydration/tests/ssr.test.tsx
@@ -143,7 +143,7 @@ describe('Server side rendering with de/rehydration', () => {
     expect(consoleMock).toHaveBeenCalledTimes(1)
     expect(fetchDataError).toHaveBeenCalledTimes(1)
     expect(el.innerHTML).toBe(expectedMarkup)
-    await sleep(10)
+    await sleep(50)
     expect(fetchDataError).toHaveBeenCalledTimes(2)
     expect(el.innerHTML).toBe(
       'ErrorComponent - status:error fetching:false data:undefined'
@@ -208,7 +208,7 @@ describe('Server side rendering with de/rehydration', () => {
     expect(consoleMock).not.toHaveBeenCalled()
     expect(fetchDataSuccess).toHaveBeenCalledTimes(0)
     expect(el.innerHTML).toBe(expectedMarkup)
-    await sleep(10)
+    await sleep(50)
     expect(fetchDataSuccess).toHaveBeenCalledTimes(1)
     expect(el.innerHTML).toBe(
       'SuccessComponent - status:success fetching:false data:success!'
diff --git a/src/react/tests/useInfiniteQuery.test.tsx b/src/react/tests/useInfiniteQuery.test.tsx
index 246fe51f4..e22967bd7 100644
--- a/src/react/tests/useInfiniteQuery.test.tsx
+++ b/src/react/tests/useInfiniteQuery.test.tsx
@@ -15,6 +15,7 @@ import {
   QueryClient,
   QueryCache,
 } from '../..'
+import { CancelledError } from '../../core'
 
 interface Result {
   items: number[]
@@ -720,6 +721,54 @@ describe('useInfiniteQuery', () => {
     })
   })
 
+  it('should stop fetching additional pages when the component is unmounted', async () => {
+    const key = queryKey()
+    const states: UseInfiniteQueryResult<number>[] = []
+    let fetches = 0
+
+    function List() {
+      const state = useInfiniteQuery(
+        key,
+        async ({ pageParam }) => {
+          fetches++
+          await sleep(50)
+          return Number(pageParam)
+        },
+        {
+          initialData: { pages: [1, 2, 3, 4], pageParams: [1, 2, 3, 4] },
+          getNextPageParam: lastPage => lastPage + 1,
+        }
+      )
+
+      states.push(state)
+
+      return null
+    }
+
+    function Page() {
+      const [show, setShow] = React.useState(true)
+
+      React.useEffect(() => {
+        setActTimeout(() => {
+          setShow(false)
+        }, 75)
+      }, [])
+
+      return show ? <List /> : null
+    }
+
+    renderWithClient(queryClient, <Page />)
+
+    await sleep(300)
+
+    expect(states.length).toBe(1)
+    expect(fetches).toBe(2)
+    expect(queryClient.getQueryState(key)).toMatchObject({
+      status: 'error',
+      error: expect.any(CancelledError),
+    })
+  })
+
   it('should be able to override the cursor in the fetchNextPage callback', async () => {
     const key = queryKey()
     const states: UseInfiniteQueryResult<number>[] = []