Repository: tannerlinsley/react-query (ts)

Issue:
Disabled queries (enabled: false) get affected by invalidateQueries (refetchInactive: true)
### Describe the bug

`
queryClient.invalidateQueries(queryKey, {
        refetchInactive: true,
      })
`
also refetches disabled queries (`enabled`: false)

but  - https://github.com/tannerlinsley/react-query/issues/947#issuecomment-1005020047

looks like this issue was introduce in this commit  - https://github.com/tannerlinsley/react-query/pull/949/commits/f5ae636e2ba697d56e1789d731e5d382f7b444bd

### Your minimal, reproducible example

https://codesandbox.io/s/quiet-dust-hegny?file=/src/App.js

### Steps to reproduce

1. Create a query with `useQuery` and set `enabled` option to false.
2. Invalidate this query with `invalidateQueries` and set `refetchInactive` to true

### Expected behavior

I expected that disabled queries (`enabled: false`) would not refetch when I use `invalidateQueries`

### Platform

- OS: macOs
- Browser: Not related

### react-query version

v3.34.8

### TypeScript version

v4.2.2


Pull request:
fix(core): do not refetch disabled queries
with refetchQueries or invalidateQueries + refetchType "inactive"

disabled queries (=queries that have observers which are all enabled:false) are matched as "inactive"; this is okay when searching for them via findAll or for removeQueries, but the docs clearly state that refetchQueries / invalidateQueries do not refetch disabled queries, and that the only way to refetch them is via refetch returned from useQuery; this is important when using enabled to signal that some dependencies are not yet ready

some tests needed to be adapted because we used disabled observer + refetchQueries a lot. The easiest way to emulate the observers we wanted here was mostly with initialData + staleTime, and to get a real inactive query, we just need to subscribe + unsubscribe immediately

closes #3202 

Patch:
diff --git a/docs/src/pages/reference/useIsMutating.md b/docs/src/pages/reference/useIsMutating.md
index 93e323345..d9a6ad4cc 100644
--- a/docs/src/pages/reference/useIsMutating.md
+++ b/docs/src/pages/reference/useIsMutating.md
@@ -15,6 +15,7 @@ const isMutatingPosts = useIsMutating(['posts'])
 
 **Options**
 
+- `mutationKey?: string | unknown[]`
 - `filters?: MutationFilters`: [Mutation Filters](../guides/filters#mutation-filters)
 
 **Returns**
diff --git a/src/core/query.ts b/src/core/query.ts
index 195cfa556..06935088d 100644
--- a/src/core/query.ts
+++ b/src/core/query.ts
@@ -250,6 +250,10 @@ export class Query<
     return this.observers.some(observer => observer.options.enabled !== false)
   }
 
+  isDisabled(): boolean {
+    return this.getObserversCount() > 0 && !this.isActive()
+  }
+
   isStale(): boolean {
     return (
       this.state.isInvalidated ||
diff --git a/src/core/queryClient.ts b/src/core/queryClient.ts
index d5f7b2662..7e2fbc02a 100644
--- a/src/core/queryClient.ts
+++ b/src/core/queryClient.ts
@@ -283,13 +283,16 @@ export class QueryClient {
     const [filters, options] = parseFilterArgs(arg1, arg2, arg3)
 
     const promises = notifyManager.batch(() =>
-      this.queryCache.findAll(filters).map(query =>
-        query.fetch(undefined, {
-          ...options,
-          cancelRefetch: options?.cancelRefetch ?? true,
-          meta: { refetchPage: filters?.refetchPage },
-        })
-      )
+      this.queryCache
+        .findAll(filters)
+        .filter(query => !query.isDisabled())
+        .map(query =>
+          query.fetch(undefined, {
+            ...options,
+            cancelRefetch: options?.cancelRefetch ?? true,
+            meta: { refetchPage: filters?.refetchPage },
+          })
+        )
     )
 
     let promise = Promise.all(promises).then(noop)
diff --git a/src/devtools/devtools.tsx b/src/devtools/devtools.tsx
index 7476205a7..808d59042 100644
--- a/src/devtools/devtools.tsx
+++ b/src/devtools/devtools.tsx
@@ -703,8 +703,6 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
             }}
           >
             {queries.map((query, i) => {
-              const isDisabled =
-                query.getObserversCount() > 0 && !query.isActive()
               return (
                 <div
                   key={query.queryHash || i}
@@ -747,7 +745,7 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
                   >
                     {query.getObserversCount()}
                   </div>
-                  {isDisabled ? (
+                  {query.isDisabled() ? (
                     <div
                       style={{
                         flex: '0 0 auto',
diff --git a/src/reactjs/useIsMutating.ts b/src/reactjs/useIsMutating.ts
index 3353dfc85..b3baaec00 100644
--- a/src/reactjs/useIsMutating.ts
+++ b/src/reactjs/useIsMutating.ts
@@ -1,18 +1,18 @@
 import React from 'react'
 
 import { notifyManager } from '../core/notifyManager'
-import { QueryKey } from '../core/types'
+import { MutationKey } from '../core/types'
 import { MutationFilters, parseMutationFilterArgs } from '../core/utils'
 import { useQueryClient } from './QueryClientProvider'
 
 export function useIsMutating(filters?: MutationFilters): number
 export function useIsMutating(
-  queryKey?: QueryKey,
-  filters?: MutationFilters
+  mutationKey?: MutationKey,
+  filters?: Omit<MutationFilters, 'mutationKey'>
 ): number
 export function useIsMutating(
-  arg1?: QueryKey | MutationFilters,
-  arg2?: MutationFilters
+  arg1?: MutationKey | MutationFilters,
+  arg2?: Omit<MutationFilters, 'mutationKey'>
 ): number {
   const mountedRef = React.useRef(false)
   const filters = parseMutationFilterArgs(arg1, arg2)


Test patch:
diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx
index b72f64ded..033141503 100644
--- a/src/core/tests/queryClient.test.tsx
+++ b/src/core/tests/queryClient.test.tsx
@@ -707,6 +707,41 @@ describe('queryClient', () => {
   })
 
   describe('refetchQueries', () => {
+    test('should not refetch if all observers are disabled', async () => {
+      const key = queryKey()
+      const queryFn = jest.fn()
+      await queryClient.fetchQuery(key, queryFn)
+      const observer1 = new QueryObserver(queryClient, {
+        queryKey: key,
+        queryFn,
+        enabled: false,
+      })
+      observer1.subscribe(() => undefined)
+      await queryClient.refetchQueries()
+      observer1.destroy()
+      expect(queryFn).toHaveBeenCalledTimes(1)
+    })
+    test('should refetch if at least one observer is enabled', async () => {
+      const key = queryKey()
+      const queryFn = jest.fn()
+      await queryClient.fetchQuery(key, queryFn)
+      const observer1 = new QueryObserver(queryClient, {
+        queryKey: key,
+        queryFn,
+        enabled: false,
+      })
+      const observer2 = new QueryObserver(queryClient, {
+        queryKey: key,
+        queryFn,
+        refetchOnMount: false,
+      })
+      observer1.subscribe(() => undefined)
+      observer2.subscribe(() => undefined)
+      await queryClient.refetchQueries()
+      observer1.destroy()
+      observer2.destroy()
+      expect(queryFn).toHaveBeenCalledTimes(2)
+    })
     test('should refetch all queries when no arguments are given', async () => {
       const key1 = queryKey()
       const key2 = queryKey()
@@ -716,11 +751,13 @@ describe('queryClient', () => {
       await queryClient.fetchQuery(key2, queryFn2)
       const observer1 = new QueryObserver(queryClient, {
         queryKey: key1,
-        enabled: false,
+        staleTime: Infinity,
+        initialData: 'initial',
       })
       const observer2 = new QueryObserver(queryClient, {
         queryKey: key1,
-        enabled: false,
+        staleTime: Infinity,
+        initialData: 'initial',
       })
       observer1.subscribe(() => undefined)
       observer2.subscribe(() => undefined)
@@ -964,13 +1001,14 @@ describe('queryClient', () => {
         queryKey: key1,
         queryFn: queryFn1,
         staleTime: Infinity,
-        enabled: false,
+        refetchOnMount: false,
       })
       const unsubscribe = observer.subscribe(() => undefined)
-      queryClient.invalidateQueries(key1, {
+      unsubscribe()
+
+      await queryClient.invalidateQueries(key1, {
         refetchType: 'inactive',
       })
-      unsubscribe()
       expect(queryFn1).toHaveBeenCalledTimes(2)
       expect(queryFn2).toHaveBeenCalledTimes(1)
     })
@@ -1002,23 +1040,19 @@ describe('queryClient', () => {
       let fetchCount = 0
       const observer = new QueryObserver(queryClient, {
         queryKey: key,
-        enabled: false,
+        queryFn: ({ signal }) => {
+          return new Promise(resolve => {
+            fetchCount++
+            setTimeout(() => resolve(5), 10)
+            if (signal) {
+              signal.addEventListener('abort', abortFn)
+            }
+          })
+        },
         initialData: 1,
       })
       observer.subscribe(() => undefined)
 
-      queryClient.fetchQuery(key, ({ signal }) => {
-        const promise = new Promise(resolve => {
-          fetchCount++
-          setTimeout(() => resolve(5), 10)
-          if (signal) {
-            signal.addEventListener('abort', abortFn)
-          }
-        })
-
-        return promise
-      })
-
       await queryClient.refetchQueries()
       observer.destroy()
       if (typeof AbortSignal === 'function') {
@@ -1033,23 +1067,19 @@ describe('queryClient', () => {
       let fetchCount = 0
       const observer = new QueryObserver(queryClient, {
         queryKey: key,
-        enabled: false,
+        queryFn: ({ signal }) => {
+          return new Promise(resolve => {
+            fetchCount++
+            setTimeout(() => resolve(5), 10)
+            if (signal) {
+              signal.addEventListener('abort', abortFn)
+            }
+          })
+        },
         initialData: 1,
       })
       observer.subscribe(() => undefined)
 
-      queryClient.fetchQuery(key, ({ signal }) => {
-        const promise = new Promise(resolve => {
-          fetchCount++
-          setTimeout(() => resolve(5), 10)
-          if (signal) {
-            signal.addEventListener('abort', abortFn)
-          }
-        })
-
-        return promise
-      })
-
       await queryClient.refetchQueries(undefined, { cancelRefetch: false })
       observer.destroy()
       if (typeof AbortSignal === 'function') {
diff --git a/src/reactjs/tests/useQuery.test.tsx b/src/reactjs/tests/useQuery.test.tsx
index 4433904b7..93e1ea3e5 100644
--- a/src/reactjs/tests/useQuery.test.tsx
+++ b/src/reactjs/tests/useQuery.test.tsx
@@ -1180,7 +1180,10 @@ describe('useQuery', () => {
           await sleep(1)
           return 'fetched'
         },
-        { enabled: false }
+        {
+          initialData: 'initial',
+          staleTime: Infinity,
+        }
       )
 
       results.push(result)
@@ -1266,7 +1269,7 @@ describe('useQuery', () => {
     })
   })
 
-  it('should update disabled query when updated with invalidateQueries', async () => {
+  it('should not update disabled query when refetched with refetchQueries', async () => {
     const key = queryKey()
     const states: UseQueryResult<number>[] = []
     let count = 0
@@ -1295,27 +1298,15 @@ describe('useQuery', () => {
 
     renderWithClient(queryClient, <Page />)
 
-    await sleep(100)
+    await sleep(50)
 
-    expect(states.length).toBe(3)
+    expect(states.length).toBe(1)
     expect(states[0]).toMatchObject({
       data: undefined,
       isFetching: false,
       isSuccess: false,
       isStale: true,
     })
-    expect(states[1]).toMatchObject({
-      data: undefined,
-      isFetching: true,
-      isSuccess: false,
-      isStale: true,
-    })
-    expect(states[2]).toMatchObject({
-      data: 1,
-      isFetching: false,
-      isSuccess: true,
-      isStale: true,
-    })
   })
 
   it('should not refetch disabled query when invalidated with invalidateQueries', async () => {