Repository: tannerlinsley/react-query
Pull request #1756 (2021-02-06 09:47:23)

fix: single query methods should be exact by default
Fixes https://github.com/tannerlinsley/react-query/issues/1725

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/wb66jg14i](https://vercel.com/tannerlinsley/react-query/wb66jg14i)  
✅ Preview: *In Progress*



Patch:
diff --git a/src/core/index.ts b/src/core/index.ts
index 89f6f659e..c3ecd0616 100644
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -1,3 +1,4 @@
+export { CancelledError } from './retryer'
 export { QueryCache } from './queryCache'
 export { QueryClient } from './queryClient'
 export { QueryObserver } from './queryObserver'
@@ -14,6 +15,5 @@ export { isCancelledError } from './retryer'
 
 // Types
 export * from './types'
-export type { CancelledError } from './retryer'
 export type { Query } from './query'
 export type { Logger } from './logger'
diff --git a/src/core/onlineManager.ts b/src/core/onlineManager.ts
index 922cc3e69..fc0c8362e 100644
--- a/src/core/onlineManager.ts
+++ b/src/core/onlineManager.ts
@@ -45,7 +45,14 @@ class OnlineManager extends Subscribable {
       return this.online
     }
 
-    return navigator.onLine === undefined || navigator.onLine
+    if (
+      typeof navigator === 'undefined' ||
+      typeof navigator.onLine === 'undefined'
+    ) {
+      return true
+    }
+
+    return navigator.onLine
   }
 
   private setDefaultEventListener() {
diff --git a/src/core/query.ts b/src/core/query.ts
index 6f8610c20..8e6aef538 100644
--- a/src/core/query.ts
+++ b/src/core/query.ts
@@ -135,7 +135,7 @@ export class Query<
   private cache: QueryCache
   private promise?: Promise<TData>
   private gcTimeout?: number
-  private retryer?: Retryer<unknown, TError>
+  private retryer?: Retryer<TData, TError>
   private observers: QueryObserver<any, any, any, any>[]
   private defaultOptions?: QueryOptions<TQueryFnData, TError, TData>
 
@@ -387,28 +387,21 @@ export class Query<
 
     // Try to fetch the data
     this.retryer = new Retryer({
-      fn: context.fetchFn,
-      onFail: () => {
-        this.dispatch({ type: 'failed' })
-      },
-      onPause: () => {
-        this.dispatch({ type: 'pause' })
-      },
-      onContinue: () => {
-        this.dispatch({ type: 'continue' })
-      },
-      retry: context.options.retry,
-      retryDelay: context.options.retryDelay,
-    })
+      fn: context.fetchFn as () => TData,
+      onSuccess: data => {
+        this.setData(data as TData)
 
-    this.promise = this.retryer.promise
-      .then(data => this.setData(data as TData))
-      .catch(error => {
-        // Set error state if needed
+        // Remove query after fetching if cache time is 0
+        if (this.cacheTime === 0) {
+          this.optionalRemove()
+        }
+      },
+      onError: error => {
+        // Optimistically update state if needed
         if (!(isCancelledError(error) && error.silent)) {
           this.dispatch({
             type: 'error',
-            error,
+            error: error as TError,
           })
         }
 
@@ -426,18 +419,21 @@ export class Query<
         if (this.cacheTime === 0) {
           this.optionalRemove()
         }
+      },
+      onFail: () => {
+        this.dispatch({ type: 'failed' })
+      },
+      onPause: () => {
+        this.dispatch({ type: 'pause' })
+      },
+      onContinue: () => {
+        this.dispatch({ type: 'continue' })
+      },
+      retry: context.options.retry,
+      retryDelay: context.options.retryDelay,
+    })
 
-        // Propagate error
-        throw error
-      })
-      .then(data => {
-        // Remove query after fetching if cache time is 0
-        if (this.cacheTime === 0) {
-          this.optionalRemove()
-        }
-
-        return data
-      })
+    this.promise = this.retryer.promise
 
     return this.promise
   }
diff --git a/src/core/queryCache.ts b/src/core/queryCache.ts
index 5c4829535..e323e1d6d 100644
--- a/src/core/queryCache.ts
+++ b/src/core/queryCache.ts
@@ -109,6 +109,11 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
     arg2?: QueryFilters
   ): Query<TQueryFnData, TError, TData> | undefined {
     const [filters] = parseFilterArgs(arg1, arg2)
+
+    if (typeof filters.exact === 'undefined') {
+      filters.exact = true
+    }
+
     return this.queries.find(query => matchQuery(filters, query))
   }
 
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index 13938952c..4c3518d15 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -49,6 +49,12 @@ export class QueryObserver<
   private currentQuery!: Query<TQueryFnData, TError, TQueryData>
   private currentResult!: QueryObserverResult<TData, TError>
   private currentResultState?: QueryState<TQueryData, TError>
+  private previousOptions?: QueryObserverOptions<
+    TQueryFnData,
+    TError,
+    TData,
+    TQueryData
+  >
   private previousQueryResult?: QueryObserverResult<TData, TError>
   private initialDataUpdateCount: number
   private initialErrorUpdateCount: number
@@ -155,8 +161,7 @@ export class QueryObserver<
   setOptions(
     options?: QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>
   ): void {
-    const prevOptions = this.options
-
+    this.previousOptions = this.options
     this.options = this.client.defaultQueryObserverOptions(options)
 
     if (
@@ -168,39 +173,49 @@ export class QueryObserver<
 
     // Keep previous query key if the user does not supply one
     if (!this.options.queryKey) {
-      this.options.queryKey = prevOptions.queryKey
+      this.options.queryKey = this.previousOptions.queryKey
     }
 
     const didUpdateQuery = this.updateQuery()
 
     let optionalFetch
+    let updateResult
     let updateStaleTimeout
     let updateRefetchInterval
 
-    // If we subscribed to a new query, optionally fetch and update intervals
+    // If we subscribed to a new query, optionally fetch and update result and timers
     if (didUpdateQuery) {
       optionalFetch = true
+      updateResult = true
       updateStaleTimeout = true
       updateRefetchInterval = true
     }
 
     // Optionally fetch if the query became enabled
-    if (this.options.enabled !== false && prevOptions.enabled === false) {
+    if (
+      this.options.enabled !== false &&
+      this.previousOptions.enabled === false
+    ) {
       optionalFetch = true
     }
 
+    // Update result if the select function changed
+    if (this.options.select !== this.previousOptions.select) {
+      updateResult = true
+    }
+
     // Update stale interval if needed
     if (
-      this.options.enabled !== prevOptions.enabled ||
-      this.options.staleTime !== prevOptions.staleTime
+      this.options.enabled !== this.previousOptions.enabled ||
+      this.options.staleTime !== this.previousOptions.staleTime
     ) {
       updateStaleTimeout = true
     }
 
     // Update refetch interval if needed
     if (
-      this.options.enabled !== prevOptions.enabled ||
-      this.options.refetchInterval !== prevOptions.refetchInterval
+      this.options.enabled !== this.previousOptions.enabled ||
+      this.options.refetchInterval !== this.previousOptions.refetchInterval
     ) {
       updateRefetchInterval = true
     }
@@ -212,8 +227,7 @@ export class QueryObserver<
       }
     }
 
-    // Update result when subscribing to a new query
-    if (didUpdateQuery) {
+    if (updateResult) {
       this.updateResult()
     }
 
@@ -399,8 +413,12 @@ export class QueryObserver<
     }
     // Select data if needed
     else if (this.options.select && typeof state.data !== 'undefined') {
-      // Use the previous select result if the query data did not change
-      if (this.currentResult && state.data === this.currentResultState?.data) {
+      // Use the previous select result if the query data and select function did not change
+      if (
+        this.currentResult &&
+        state.data === this.currentResultState?.data &&
+        this.options.select === this.previousOptions?.select
+      ) {
         data = this.currentResult.data
       } else {
         data = this.options.select(state.data)
diff --git a/src/core/retryer.ts b/src/core/retryer.ts
index 8b4b7bfbf..f637e47d0 100644
--- a/src/core/retryer.ts
+++ b/src/core/retryer.ts
@@ -6,6 +6,8 @@ import { functionalUpdate, sleep } from './utils'
 
 interface RetryerConfig<TData = unknown, TError = unknown> {
   fn: () => TData | Promise<TData>
+  onError?: (error: TError) => void
+  onSuccess?: (data: TData) => void
   onFail?: (failureCount: number, error: TError) => void
   onPause?: () => void
   onContinue?: () => void
@@ -88,15 +90,21 @@ export class Retryer<TData = unknown, TError = unknown> {
     })
 
     const resolve = (value: any) => {
-      this.isResolved = true
-      continueFn?.()
-      promiseResolve(value)
+      if (!this.isResolved) {
+        this.isResolved = true
+        config.onSuccess?.(value)
+        continueFn?.()
+        promiseResolve(value)
+      }
     }
 
     const reject = (value: any) => {
-      this.isResolved = true
-      continueFn?.()
-      promiseReject(value)
+      if (!this.isResolved) {
+        this.isResolved = true
+        config.onError?.(value)
+        continueFn?.()
+        promiseReject(value)
+      }
     }
 
     const pause = () => {
@@ -129,13 +137,15 @@ export class Retryer<TData = unknown, TError = unknown> {
 
       // Create callback to cancel this fetch
       cancelFn = cancelOptions => {
-        reject(new CancelledError(cancelOptions))
-
-        // Cancel transport if supported
-        if (isCancelable(promiseOrValue)) {
-          try {
-            promiseOrValue.cancel()
-          } catch {}
+        if (!this.isResolved) {
+          reject(new CancelledError(cancelOptions))
+
+          // Cancel transport if supported
+          if (isCancelable(promiseOrValue)) {
+            try {
+              promiseOrValue.cancel()
+            } catch {}
+          }
         }
       }
 
diff --git a/src/react/useBaseQuery.ts b/src/react/useBaseQuery.ts
index c75338b5a..b004a3687 100644
--- a/src/react/useBaseQuery.ts
+++ b/src/react/useBaseQuery.ts
@@ -1,15 +1,18 @@
 import React from 'react'
 
+import { QueryObserverResult } from '../core/types'
 import { notifyManager } from '../core/notifyManager'
 import { QueryObserver } from '../core/queryObserver'
 import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary'
 import { useQueryClient } from './QueryClientProvider'
 import { UseBaseQueryOptions } from './types'
+import { useIsMounted } from './useIsMounted'
 
 export function useBaseQuery<TQueryFnData, TError, TData, TQueryData>(
   options: UseBaseQueryOptions<TQueryFnData, TError, TData, TQueryData>,
   Observer: typeof QueryObserver
 ) {
+  const isMounted = useIsMounted()
   const queryClient = useQueryClient()
   const errorResetBoundary = useQueryErrorResetBoundary()
   const defaultedOptions = queryClient.defaultQueryObserverOptions(options)
@@ -63,8 +66,14 @@ export function useBaseQuery<TQueryFnData, TError, TData, TQueryData>(
   // Subscribe to the observer
   React.useEffect(() => {
     errorResetBoundary.clearReset()
-    return observer.subscribe(notifyManager.batchCalls(setCurrentResult))
-  }, [observer, errorResetBoundary])
+    return observer.subscribe(
+      notifyManager.batchCalls((result: QueryObserverResult) => {
+        if (isMounted()) {
+          setCurrentResult(result)
+        }
+      })
+    )
+  }, [observer, errorResetBoundary, isMounted])
 
   // Handle suspense
   if (observer.options.suspense || observer.options.useErrorBoundary) {
diff --git a/src/react/useIsFetching.ts b/src/react/useIsFetching.ts
index 1c6eb0c50..c7c919f14 100644
--- a/src/react/useIsFetching.ts
+++ b/src/react/useIsFetching.ts
@@ -4,6 +4,7 @@ import { notifyManager } from '../core/notifyManager'
 import { QueryKey } from '../core/types'
 import { parseFilterArgs, QueryFilters } from '../core/utils'
 import { useQueryClient } from './QueryClientProvider'
+import { useIsMounted } from './useIsMounted'
 
 export function useIsFetching(filters?: QueryFilters): number
 export function useIsFetching(
@@ -14,6 +15,7 @@ export function useIsFetching(
   arg1?: QueryKey | QueryFilters,
   arg2?: QueryFilters
 ): number {
+  const isMounted = useIsMounted()
   const queryClient = useQueryClient()
   const [filters] = parseFilterArgs(arg1, arg2)
   const [isFetching, setIsFetching] = React.useState(
@@ -29,13 +31,15 @@ export function useIsFetching(
     () =>
       queryClient.getQueryCache().subscribe(
         notifyManager.batchCalls(() => {
-          const newIsFetching = queryClient.isFetching(filtersRef.current)
-          if (isFetchingRef.current !== newIsFetching) {
-            setIsFetching(newIsFetching)
+          if (isMounted()) {
+            const newIsFetching = queryClient.isFetching(filtersRef.current)
+            if (isFetchingRef.current !== newIsFetching) {
+              setIsFetching(newIsFetching)
+            }
           }
         })
       ),
-    [queryClient]
+    [queryClient, isMounted]
   )
 
   return isFetching
diff --git a/src/react/useIsMounted.ts b/src/react/useIsMounted.ts
new file mode 100644
index 000000000..f1f620f72
--- /dev/null
+++ b/src/react/useIsMounted.ts
@@ -0,0 +1,17 @@
+import React from 'react'
+
+import { isServer } from '../core/utils'
+
+export function useIsMounted() {
+  const mountedRef = React.useRef(false)
+  const isMounted = React.useCallback(() => mountedRef.current, [])
+
+  React[isServer ? 'useEffect' : 'useLayoutEffect'](() => {
+    mountedRef.current = true
+    return () => {
+      mountedRef.current = false
+    }
+  }, [])
+
+  return isMounted
+}
diff --git a/src/react/useMutation.ts b/src/react/useMutation.ts
index 7e65706b7..d5fb6374e 100644
--- a/src/react/useMutation.ts
+++ b/src/react/useMutation.ts
@@ -14,6 +14,7 @@ import {
   MutationKey,
   MutationObserverResult,
 } from '../core/types'
+import { useIsMounted } from './useIsMounted'
 
 // HOOK
 
@@ -68,6 +69,7 @@ export function useMutation<
     | UseMutationOptions<TData, TError, TVariables, TContext>,
   arg3?: UseMutationOptions<TData, TError, TVariables, TContext>
 ): UseMutationResult<TData, TError, TVariables, TContext> {
+  const isMounted = useIsMounted()
   const options = parseMutationArgs(arg1, arg2, arg3)
   const queryClient = useQueryClient()
 
@@ -96,14 +98,13 @@ export function useMutation<
           (
             result: MutationObserverResult<TData, TError, TVariables, TContext>
           ) => {
-            // Check if the component is still mounted
-            if (observer.hasListeners()) {
+            if (isMounted()) {
               setCurrentResult(result)
             }
           }
         )
       ),
-    [observer]
+    [observer, isMounted]
   )
 
   const mutate = React.useCallback<
diff --git a/src/react/useQueries.ts b/src/react/useQueries.ts
index 81774b9ae..1a85857bd 100644
--- a/src/react/useQueries.ts
+++ b/src/react/useQueries.ts
@@ -1,11 +1,14 @@
 import React from 'react'
 
+import { QueryObserverResult } from '../core/types'
 import { notifyManager } from '../core/notifyManager'
 import { QueriesObserver } from '../core/queriesObserver'
 import { useQueryClient } from './QueryClientProvider'
 import { UseQueryOptions, UseQueryResult } from './types'
+import { useIsMounted } from './useIsMounted'
 
 export function useQueries(queries: UseQueryOptions[]): UseQueryResult[] {
+  const isMounted = useIsMounted()
   const queryClient = useQueryClient()
 
   // Create queries observer
@@ -25,8 +28,15 @@ export function useQueries(queries: UseQueryOptions[]): UseQueryResult[] {
 
   // Subscribe to the observer
   React.useEffect(
-    () => observer.subscribe(notifyManager.batchCalls(setCurrentResult)),
-    [observer]
+    () =>
+      observer.subscribe(
+        notifyManager.batchCalls((result: QueryObserverResult[]) => {
+          if (isMounted()) {
+            setCurrentResult(result)
+          }
+        })
+      ),
+    [observer, isMounted]
   )
 
   return currentResult


Test patch:
diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx
index a754a7d0b..1de12ad59 100644
--- a/src/core/tests/queryClient.test.tsx
+++ b/src/core/tests/queryClient.test.tsx
@@ -153,6 +153,25 @@ describe('queryClient', () => {
     })
   })
 
+  describe('getQueryData', () => {
+    test('should return the query data if the query is found', () => {
+      const key = queryKey()
+      queryClient.setQueryData([key, 'id'], 'bar')
+      expect(queryClient.getQueryData([key, 'id'])).toBe('bar')
+    })
+
+    test('should return undefined if the query is not found', () => {
+      const key = queryKey()
+      expect(queryClient.getQueryData(key)).toBeUndefined()
+    })
+
+    test('should match exact by default', () => {
+      const key = queryKey()
+      queryClient.setQueryData([key, 'id'], 'bar')
+      expect(queryClient.getQueryData([key])).toBeUndefined()
+    })
+  })
+
   describe('fetchQuery', () => {
     // https://github.com/tannerlinsley/react-query/issues/652
     test('should not retry by default', async () => {
diff --git a/src/core/tests/queryObserver.test.tsx b/src/core/tests/queryObserver.test.tsx
index a540f98ea..8ffa73cb6 100644
--- a/src/core/tests/queryObserver.test.tsx
+++ b/src/core/tests/queryObserver.test.tsx
@@ -130,6 +130,71 @@ describe('queryObserver', () => {
     expect(observerResult2.data).toMatchObject({ myCount: 1 })
   })
 
+  test('should run the selector again if the selector changed', async () => {
+    const key = queryKey()
+    let count = 0
+    const results: QueryObserverResult[] = []
+    const queryFn = () => ({ count: 1 })
+    const select1 = (data: ReturnType<typeof queryFn>) => {
+      count++
+      return { myCount: data.count }
+    }
+    const select2 = (_data: ReturnType<typeof queryFn>) => {
+      count++
+      return { myCount: 99 }
+    }
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn,
+      select: select1,
+    })
+    const unsubscribe = observer.subscribe(result => {
+      results.push(result)
+    })
+    await sleep(1)
+    observer.setOptions({
+      queryKey: key,
+      queryFn,
+      select: select2,
+    })
+    await sleep(1)
+    unsubscribe()
+    expect(count).toBe(2)
+    expect(results.length).toBe(2)
+    expect(results[0]).toMatchObject({ data: { myCount: 1 } })
+    expect(results[1]).toMatchObject({ data: { myCount: 99 } })
+  })
+
+  test('should not run the selector again if the data and selector did not change', async () => {
+    const key = queryKey()
+    let count = 0
+    const results: QueryObserverResult[] = []
+    const queryFn = () => ({ count: 1 })
+    const select = (data: ReturnType<typeof queryFn>) => {
+      count++
+      return { myCount: data.count }
+    }
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn,
+      select,
+    })
+    const unsubscribe = observer.subscribe(result => {
+      results.push(result)
+    })
+    await sleep(1)
+    observer.setOptions({
+      queryKey: key,
+      queryFn,
+      select,
+    })
+    await sleep(1)
+    unsubscribe()
+    expect(count).toBe(1)
+    expect(results.length).toBe(1)
+    expect(results[0]).toMatchObject({ data: { myCount: 1 } })
+  })
+
   test('should not run the selector again if the data did not change', async () => {
     const key = queryKey()
     let count = 0
diff --git a/src/react/tests/useInfiniteQuery.test.tsx b/src/react/tests/useInfiniteQuery.test.tsx
index ddc1e25f2..246fe51f4 100644
--- a/src/react/tests/useInfiniteQuery.test.tsx
+++ b/src/react/tests/useInfiniteQuery.test.tsx
@@ -874,6 +874,71 @@ describe('useInfiniteQuery', () => {
     })
   })
 
+  it('should only refetch the first page when initialData is provided', async () => {
+    const key = queryKey()
+    const states: UseInfiniteQueryResult<number>[] = []
+
+    function Page() {
+      const state = useInfiniteQuery(
+        key,
+        async ({ pageParam }) => {
+          await sleep(10)
+          return pageParam
+        },
+        {
+          initialData: { pages: [1], pageParams: [1] },
+          getNextPageParam: lastPage => lastPage + 1,
+        }
+      )
+
+      states.push(state)
+
+      const { fetchNextPage } = state
+
+      React.useEffect(() => {
+        setActTimeout(() => {
+          fetchNextPage()
+        }, 20)
+      }, [fetchNextPage])
+
+      return null
+    }
+
+    renderWithClient(queryClient, <Page />)
+
+    await sleep(100)
+
+    expect(states.length).toBe(4)
+    expect(states[0]).toMatchObject({
+      data: { pages: [1] },
+      hasNextPage: true,
+      isFetching: true,
+      isFetchingNextPage: false,
+      isSuccess: true,
+    })
+    expect(states[1]).toMatchObject({
+      data: { pages: [1] },
+      hasNextPage: true,
+      isFetching: false,
+      isFetchingNextPage: false,
+      isSuccess: true,
+    })
+    expect(states[2]).toMatchObject({
+      data: { pages: [1] },
+      hasNextPage: true,
+      isFetching: true,
+      isFetchingNextPage: true,
+      isSuccess: true,
+    })
+    expect(states[3]).toMatchObject({
+      data: { pages: [1, 2] },
+      hasNextPage: true,
+      isFetching: false,
+      isFetchingNextPage: false,
+      isSuccess: true,
+    })
+  })
+
   it('should set hasNextPage to false if getNextPageParam returns undefined', async () => {
     const key = queryKey()
     const states: UseInfiniteQueryResult<number>[] = []
diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx
index 41ef4c869..8ac67e533 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -13,6 +13,7 @@ import {
 } from './utils'
 import {
   useQuery,
+  CancelledError,
   QueryClient,
   UseQueryResult,
   QueryCache,
@@ -900,7 +901,7 @@ describe('useQuery', () => {
 
     renderWithClient(queryClient, <Page />)
 
-    await waitFor(() => renderCount > 1)
+    await sleep(10)
     expect(renderCount).toBe(2)
     expect(states.length).toBe(2)
     expect(states[0]).toMatchObject({ data: undefined })
@@ -1051,7 +1052,14 @@ describe('useQuery', () => {
     queryClient.setQueryData(key, 'set')
 
     function Page() {
-      const result = useQuery(key, () => 'fetched', { enabled: false })
+      const result = useQuery(
+        key,
+        async () => {
+          await sleep(1)
+          return 'fetched'
+        },
+        { enabled: false }
+      )
 
       results.push(result)
 
@@ -1082,7 +1090,8 @@ describe('useQuery', () => {
     function Page() {
       const state = useQuery(
         key,
-        () => {
+        async () => {
+          await sleep(1)
           count++
           return count
         },
@@ -2214,10 +2223,17 @@ describe('useQuery', () => {
     let count = 0
 
     function Page() {
-      const state = useQuery(key, () => count++, {
-        staleTime: Infinity,
-        refetchOnWindowFocus: 'always',
-      })
+      const state = useQuery(
+        key,
+        async () => {
+          await sleep(1)
+          return count++
+        },
+        {
+          staleTime: Infinity,
+          refetchOnWindowFocus: 'always',
+        }
+      )
       states.push(state)
       return null
     }
@@ -2725,7 +2741,10 @@ describe('useQuery', () => {
     queryClient.setQueryData(key, 'prefetched')
 
     function Page() {
-      const state = useQuery(key, () => 'data')
+      const state = useQuery(key, async () => {
+        await sleep(1)
+        return 'data'
+      })
       states.push(state)
       return null
     }
@@ -3269,6 +3288,70 @@ describe('useQuery', () => {
     expect(cancelFn).toHaveBeenCalled()
   })
 
+  it('should refetch when quickly switching to a failed query', async () => {
+    const key = queryKey()
+    const states: UseQueryResult<string>[] = []
+
+    const queryFn = () => {
+      let cancelFn = jest.fn()
+
+      const promise = new Promise<string>((resolve, reject) => {
+        cancelFn = jest.fn(() => reject('Cancelled'))
+        sleep(50).then(() => resolve('OK'))
+      })
+
+      ;(promise as any).cancel = cancelFn
+
+      return promise
+    }
+
+    function Page() {
+      const [id, setId] = React.useState(1)
+      const [hasChanged, setHasChanged] = React.useState(false)
+
+      const state = useQuery([key, id], queryFn)
+
+      states.push(state)
+
+      React.useEffect(() => {
+        setId(prevId => (prevId === 1 ? 2 : 1))
+        setHasChanged(true)
+      }, [hasChanged])
+
+      return null
+    }
+
+    renderWithClient(queryClient, <Page />)
+
+    await sleep(100)
+    expect(states.length).toBe(5)
+    // Load query 1
+    expect(states[0]).toMatchObject({
+      status: 'loading',
+      error: null,
+    })
+    // Load query 2
+    expect(states[1]).toMatchObject({
+      status: 'loading',
+      error: null,
+    })
+    // Load query 1
+    expect(states[2]).toMatchObject({
+      status: 'loading',
+      error: expect.any(CancelledError),
+    })
+    // State update
+    expect(states[3]).toMatchObject({
+      status: 'loading',
+      error: expect.any(CancelledError),
+    })
+    // Loaded query 1
+    expect(states[4]).toMatchObject({
+      status: 'success',
+      error: null,
+    })
+  })
+
   it('should update query state and refetch when reset with resetQueries', async () => {
     const key = queryKey()
     const states: UseQueryResult<number>[] = []
@@ -3277,7 +3360,8 @@ describe('useQuery', () => {
     function Page() {
       const state = useQuery(
         key,
-        () => {
+        async () => {
+          await sleep(1)
           count++
           return count
         },