Repository: tannerlinsley/react-query (ts)

Issue:
useIsFetching always returns false
### Describe the bug

`useIsFetching` always shows zero when loading for the first time.

### Your minimal, reproducible example

https://codesandbox.io/s/react-query-starting-point-forked-zz56e1?file=/src/App.tsx

### Steps to reproduce

look at the sandbox
isFetching should be true at some point, but it's not

note that it works if you change the order of  `Example` and `GlobalFetchingIndicator`

### Expected behavior

`useIsFetching` should show the correct fetching count

### How often does this bug happen?

Every time

### Screenshots or Videos

_No response_

### Platform

all of them

### react-query version

v3 and v4

### TypeScript version

_No response_

### Additional context

it seems that subscription order is important. We check for `isFetching` when the hook mounts, and then again inside the subscription in the effect. However:

- on mount, we are not (yet) fetching
- when the effect runs and subscribes, we don't immediately receive an event, because the fetch was already triggered (useEffect execution order)
- the next even we receive is when the query has finished loading, so we are again not fetching

---

A potential solution could be to check for `queryClient.isFetching` once again if the effect runs, but outside of the subscription.

Pull request:
fix(useIsFetching): account for fetches happening in the same render cycle
fixes #3437

if a fetch happens in the same render cycle as useIsFetching is mounting, but it is mounted after the query, what happens is that:

- the initial state of `isFetching` is `0` because the effect to fetch the query hasn't started yet
- the query effect runs before the isFetching effect, which means we miss the event, thus the next event we get from the subscription gives us 0 again

so we need to check for isFetching in the effect that runs to set up the subscription as well. This catches fetches that basically started in the same effect cycle, but before "our" effect ran

Patch:
diff --git a/src/react/useIsFetching.ts b/src/react/useIsFetching.ts
index a1319c53d..b104fe148 100644
--- a/src/react/useIsFetching.ts
+++ b/src/react/useIsFetching.ts
@@ -3,8 +3,21 @@ import React from 'react'
 import { notifyManager } from '../core/notifyManager'
 import { QueryKey } from '../core/types'
 import { parseFilterArgs, QueryFilters } from '../core/utils'
+import { QueryClient } from '../core'
 import { useQueryClient } from './QueryClientProvider'
 
+const checkIsFetching = (
+  queryClient: QueryClient,
+  filters: QueryFilters,
+  isFetching: number,
+  setIsFetching: React.Dispatch<React.SetStateAction<number>>
+) => {
+  const newIsFetching = queryClient.isFetching(filters)
+  if (isFetching !== newIsFetching) {
+    setIsFetching(newIsFetching)
+  }
+}
+
 export function useIsFetching(filters?: QueryFilters): number
 export function useIsFetching(
   queryKey?: QueryKey,
@@ -31,13 +44,22 @@ export function useIsFetching(
   React.useEffect(() => {
     mountedRef.current = true
 
+    checkIsFetching(
+      queryClient,
+      filtersRef.current,
+      isFetchingRef.current,
+      setIsFetching
+    )
+
     const unsubscribe = queryClient.getQueryCache().subscribe(
       notifyManager.batchCalls(() => {
         if (mountedRef.current) {
-          const newIsFetching = queryClient.isFetching(filtersRef.current)
-          if (isFetchingRef.current !== newIsFetching) {
-            setIsFetching(newIsFetching)
-          }
+          checkIsFetching(
+            queryClient,
+            filtersRef.current,
+            isFetchingRef.current,
+            setIsFetching
+          )
         }
       })
     )


Test patch:
diff --git a/src/react/tests/useIsFetching.test.tsx b/src/react/tests/useIsFetching.test.tsx
index 085242cbf..16c83156f 100644
--- a/src/react/tests/useIsFetching.test.tsx
+++ b/src/react/tests/useIsFetching.test.tsx
@@ -100,7 +100,7 @@ describe('useIsFetching', () => {
     }
 
     renderWithClient(queryClient, <Page />)
-    await waitFor(() => expect(isFetchings).toEqual([0, 0, 1, 2, 1, 0]))
+    await waitFor(() => expect(isFetchings).toEqual([0, 1, 1, 2, 1, 0]))
     expect(consoleMock).not.toHaveBeenCalled()
     expect(consoleMock.mock.calls[0]?.[0] ?? '').not.toMatch('setState')
 
@@ -159,4 +159,29 @@ describe('useIsFetching', () => {
     await sleep(100)
     expect(isFetchings).toEqual([0, 0, 1, 0])
   })
+
+  it('should show the correct fetching state when mounted after a query', async () => {
+    const queryClient = new QueryClient()
+    const key = queryKey()
+
+    function Page() {
+      useQuery(key, async () => {
+        await sleep(10)
+        return 'test'
+      })
+
+      const isFetching = useIsFetching()
+
+      return (
+        <div>
+          <div>isFetching: {isFetching}</div>
+        </div>
+      )
+    }
+
+    const rendered = renderWithClient(queryClient, <Page />)
+
+    await rendered.findByText('isFetching: 1')
+    await rendered.findByText('isFetching: 0')
+  })
 })