From c4a8a1267e44b4d23fd44f4ed4a03fa8c0fb234d Mon Sep 17 00:00:00 2001
From: Corbin Crutchley <git@crutchcorn.dev>
Date: Tue, 23 Jan 2024 10:26:21 -0800
Subject: [PATCH 1/6] feat: add useQueryState hook

---
 packages/react-query/src/index.ts         |  1 +
 packages/react-query/src/useIsFetching.ts | 21 ++-----
 packages/react-query/src/useQueryState.ts | 70 +++++++++++++++++++++++
 3 files changed, 76 insertions(+), 16 deletions(-)
 create mode 100644 packages/react-query/src/useQueryState.ts

diff --git a/packages/react-query/src/index.ts b/packages/react-query/src/index.ts
index 926c673a6d..099ebeb9a4 100644
--- a/packages/react-query/src/index.ts
+++ b/packages/react-query/src/index.ts
@@ -40,6 +40,7 @@ export {
 } from './QueryErrorResetBoundary'
 export { useIsFetching } from './useIsFetching'
 export { useIsMutating, useMutationState } from './useMutationState'
+export { useQueryState } from './useQueryState'
 export { useMutation } from './useMutation'
 export { useInfiniteQuery } from './useInfiniteQuery'
 export { useIsRestoring, IsRestoringProvider } from './isRestoring'
diff --git a/packages/react-query/src/useIsFetching.ts b/packages/react-query/src/useIsFetching.ts
index a6252912f2..cb7045d8d2 100644
--- a/packages/react-query/src/useIsFetching.ts
+++ b/packages/react-query/src/useIsFetching.ts
@@ -1,24 +1,13 @@
 'use client'
-import * as React from 'react'
-import { notifyManager } from '@tanstack/query-core'
-
-import { useQueryClient } from './QueryClientProvider'
+import { useQueryState } from './useQueryState'
 import type { QueryClient, QueryFilters } from '@tanstack/query-core'
 
 export function useIsFetching(
   filters?: QueryFilters,
   queryClient?: QueryClient,
 ): number {
-  const client = useQueryClient(queryClient)
-  const queryCache = client.getQueryCache()
-
-  return React.useSyncExternalStore(
-    React.useCallback(
-      (onStoreChange) =>
-        queryCache.subscribe(notifyManager.batchCalls(onStoreChange)),
-      [queryCache],
-    ),
-    () => client.isFetching(filters),
-    () => client.isFetching(filters),
-  )
+  return useQueryState(
+    { filters: { ...filters, fetchStatus: 'fetching' } },
+    queryClient,
+  ).length
 }
diff --git a/packages/react-query/src/useQueryState.ts b/packages/react-query/src/useQueryState.ts
new file mode 100644
index 0000000000..dc90e6d90a
--- /dev/null
+++ b/packages/react-query/src/useQueryState.ts
@@ -0,0 +1,70 @@
+'use client'
+import * as React from 'react'
+
+import { notifyManager, replaceEqualDeep } from '@tanstack/query-core'
+import { useQueryClient } from './QueryClientProvider'
+import type {
+  DefaultError,
+  Query,
+  QueryCache,
+  QueryClient,
+  QueryFilters,
+  QueryKey,
+  QueryState,
+} from '@tanstack/query-core'
+
+type QueryStateOptions<TResult = QueryState> = {
+  filters?: QueryFilters
+  select?: (query: Query<unknown, DefaultError, unknown, QueryKey>) => TResult
+}
+
+function getResult<TResult = QueryState>(
+  queryCache: QueryCache,
+  options: QueryStateOptions<TResult>,
+): Array<TResult> {
+  return queryCache
+    .findAll(options.filters)
+    .map(
+      (query): TResult =>
+        (options.select
+          ? options.select(
+              query as Query<unknown, DefaultError, unknown, QueryKey>,
+            )
+          : query.state) as TResult,
+    )
+}
+
+export function useQueryState<TResult = QueryState>(
+  options: QueryStateOptions<TResult> = {},
+  queryClient?: QueryClient,
+): Array<TResult> {
+  const queryCache = useQueryClient(queryClient).getQueryCache()
+  const optionsRef = React.useRef(options)
+  const result = React.useRef<Array<TResult>>()
+  if (!result.current) {
+    result.current = getResult(queryCache, options)
+  }
+
+  React.useEffect(() => {
+    optionsRef.current = options
+  })
+
+  return React.useSyncExternalStore(
+    React.useCallback(
+      (onStoreChange) =>
+        queryCache.subscribe(() => {
+          const nextResult = replaceEqualDeep(
+            result.current,
+            getResult(queryCache, optionsRef.current),
+          )
+          if (result.current !== nextResult) {
+            result.current = nextResult
+            notifyManager.schedule(onStoreChange)
+          }
+        }),
+      [queryCache],
+    ),
+    () => result.current,
+    () => result.current,
+  )!
+}

From b8d68514fa38182426f2e959c2d40e63ec798af2 Mon Sep 17 00:00:00 2001
From: Corbin Crutchley <git@crutchcorn.dev>
Date: Tue, 23 Jan 2024 13:22:11 -0800
Subject: [PATCH 2/6] test: improve useIsFetching tests

---
 .../src/__tests__/useIsFetching.test.tsx       | 18 ++++++++++++++++--
 1 file changed, 16 insertions(+), 2 deletions(-)

diff --git a/packages/react-query/src/__tests__/useIsFetching.test.tsx b/packages/react-query/src/__tests__/useIsFetching.test.tsx
index c80261c6b5..7eabbf2036 100644
--- a/packages/react-query/src/__tests__/useIsFetching.test.tsx
+++ b/packages/react-query/src/__tests__/useIsFetching.test.tsx
@@ -176,11 +176,16 @@ describe('useIsFetching', () => {
     const queryClient = createQueryClient()
     const key = queryKey()
 
+    let resolve!: () => void
+    const promise = new Promise<void>((_resolve) => {
+      resolve = _resolve
+    })
+
     function Page() {
       useQuery({
         queryKey: key,
         queryFn: async () => {
-          await sleep(10)
+          await promise
           return 'test'
         },
       })
@@ -197,6 +202,9 @@ describe('useIsFetching', () => {
     const rendered = renderWithClient(queryClient, <Page />)
 
     await rendered.findByText('isFetching: 1')
+
+    resolve()
+
     await rendered.findByText('isFetching: 0')
   })
 
@@ -204,12 +212,17 @@ describe('useIsFetching', () => {
     const queryClient = createQueryClient()
     const key = queryKey()
 
+    let resolve!: () => void
+    const promise = new Promise<void>((_resolve) => {
+      resolve = _resolve
+    })
+
     function Page() {
       useQuery(
         {
           queryKey: key,
           queryFn: async () => {
-            await sleep(10)
+            await promise
             return 'test'
           },
         },
@@ -228,5 +241,6 @@ describe('useIsFetching', () => {
     const rendered = render(<Page></Page>)
 
     await waitFor(() => rendered.getByText('isFetching: 1'))
+    resolve()
   })
 })

From 4f000317b618878aaabd91d21695f74ffcd0808f Mon Sep 17 00:00:00 2001
From: Dominik Dorfmeister <office@dorfmeister.cc>
Date: Wed, 24 Jan 2024 10:20:10 +0100
Subject: [PATCH 3/6] fix: useQueryState impl

the value of the ref can get out of sync if we only write the ref in the subscribe function
---
 .../src/__tests__/useIsFetching.test.tsx      | 22 +++-----------
 packages/react-query/src/useQueryState.ts     | 29 +++++++++----------
 2 files changed, 17 insertions(+), 34 deletions(-)

diff --git a/packages/react-query/src/__tests__/useIsFetching.test.tsx b/packages/react-query/src/__tests__/useIsFetching.test.tsx
index 7eabbf2036..aec5b027ee 100644
--- a/packages/react-query/src/__tests__/useIsFetching.test.tsx
+++ b/packages/react-query/src/__tests__/useIsFetching.test.tsx
@@ -176,16 +176,11 @@ describe('useIsFetching', () => {
     const queryClient = createQueryClient()
     const key = queryKey()
 
-    let resolve!: () => void
-    const promise = new Promise<void>((_resolve) => {
-      resolve = _resolve
-    })
-
     function Page() {
       useQuery({
         queryKey: key,
         queryFn: async () => {
-          await promise
+          await sleep(100)
           return 'test'
         },
       })
@@ -202,9 +197,6 @@ describe('useIsFetching', () => {
     const rendered = renderWithClient(queryClient, <Page />)
 
     await rendered.findByText('isFetching: 1')
-
-    resolve()
-
     await rendered.findByText('isFetching: 0')
   })
 
@@ -212,25 +204,20 @@ describe('useIsFetching', () => {
     const queryClient = createQueryClient()
     const key = queryKey()
 
-    let resolve!: () => void
-    const promise = new Promise<void>((_resolve) => {
-      resolve = _resolve
-    })
-
     function Page() {
+      const isFetching = useIsFetching({}, queryClient)
+
       useQuery(
         {
           queryKey: key,
           queryFn: async () => {
-            await promise
+            await sleep(10)
             return 'test'
           },
         },
         queryClient,
       )
 
-      const isFetching = useIsFetching({}, queryClient)
-
       return (
         <div>
           <div>isFetching: {isFetching}</div>
@@ -241,6 +228,5 @@ describe('useIsFetching', () => {
     const rendered = render(<Page></Page>)
 
     await waitFor(() => rendered.getByText('isFetching: 1'))
-    resolve()
   })
 })
diff --git a/packages/react-query/src/useQueryState.ts b/packages/react-query/src/useQueryState.ts
index dc90e6d90a..81e722e092 100644
--- a/packages/react-query/src/useQueryState.ts
+++ b/packages/react-query/src/useQueryState.ts
@@ -26,11 +26,7 @@ function getResult<TResult = QueryState>(
     .findAll(options.filters)
     .map(
       (query): TResult =>
-        (options.select
-          ? options.select(
-              query as Query<unknown, DefaultError, unknown, QueryKey>,
-            )
-          : query.state) as TResult,
+        (options.select ? options.select(query) : query.state) as TResult,
     )
 }
 
@@ -52,19 +48,20 @@ export function useQueryState<TResult = QueryState>(
   return React.useSyncExternalStore(
     React.useCallback(
       (onStoreChange) =>
-        queryCache.subscribe(() => {
-          const nextResult = replaceEqualDeep(
-            result.current,
-            getResult(queryCache, optionsRef.current),
-          )
-          if (result.current !== nextResult) {
-            result.current = nextResult
-            notifyManager.schedule(onStoreChange)
-          }
-        }),
+        queryCache.subscribe(notifyManager.batchCalls(onStoreChange)),
       [queryCache],
     ),
-    () => result.current,
+    () => {
+      const nextResult = replaceEqualDeep(
+        result.current,
+        getResult(queryCache, optionsRef.current),
+      )
+      if (result.current !== nextResult) {
+        result.current = nextResult
+      }
+
+      return result.current
+    },
     () => result.current,
   )!
 }

From ff658ee94bcf657440968915939946e6a96e3c32 Mon Sep 17 00:00:00 2001
From: Corbin Crutchley <git@crutchcorn.dev>
Date: Wed, 24 Jan 2024 05:31:21 -0800
Subject: [PATCH 4/6] chore: refactor useMutationState, fix tests

---
 .../src/__tests__/useMutationState.test.tsx   |  9 ++++++--
 packages/react-query/src/useMutationState.ts  | 23 ++++++++++---------
 2 files changed, 19 insertions(+), 13 deletions(-)

diff --git a/packages/react-query/src/__tests__/useMutationState.test.tsx b/packages/react-query/src/__tests__/useMutationState.test.tsx
index 5f09a7aa17..643ced0c2a 100644
--- a/packages/react-query/src/__tests__/useMutationState.test.tsx
+++ b/packages/react-query/src/__tests__/useMutationState.test.tsx
@@ -66,12 +66,15 @@ describe('useIsMutating', () => {
     const isMutatings: Array<number> = []
     const queryClient = createQueryClient()
 
-    function IsMutating() {
+    function IsMutatingBase() {
       const isMutating = useIsMutating({ mutationKey: ['mutation1'] })
       isMutatings.push(isMutating)
       return null
     }
 
+    // Memo to avoid other `useMutation` hook causing a re-render
+    const IsMutating = React.memo(IsMutatingBase)
+
     function Page() {
       const { mutate: mutate1 } = useMutation({
         mutationKey: ['mutation1'],
@@ -104,7 +107,7 @@ describe('useIsMutating', () => {
     const isMutatings: Array<number> = []
     const queryClient = createQueryClient()
 
-    function IsMutating() {
+    function IsMutatingBase() {
       const isMutating = useIsMutating({
         predicate: (mutation) =>
           mutation.options.mutationKey?.[0] === 'mutation1',
@@ -113,6 +116,8 @@ describe('useIsMutating', () => {
       return null
     }
 
+    const IsMutating = React.memo(IsMutatingBase);
+
     function Page() {
       const { mutate: mutate1 } = useMutation({
         mutationKey: ['mutation1'],
diff --git a/packages/react-query/src/useMutationState.ts b/packages/react-query/src/useMutationState.ts
index d14ebc46b7..61054a8180 100644
--- a/packages/react-query/src/useMutationState.ts
+++ b/packages/react-query/src/useMutationState.ts
@@ -64,19 +64,20 @@ export function useMutationState<TResult = MutationState>(
   return React.useSyncExternalStore(
     React.useCallback(
       (onStoreChange) =>
-        mutationCache.subscribe(() => {
-          const nextResult = replaceEqualDeep(
-            result.current,
-            getResult(mutationCache, optionsRef.current),
-          )
-          if (result.current !== nextResult) {
-            result.current = nextResult
-            notifyManager.schedule(onStoreChange)
-          }
-        }),
+        mutationCache.subscribe(notifyManager.batchCalls(onStoreChange)),
       [mutationCache],
     ),
-    () => result.current,
+    () => {
+      const nextResult = replaceEqualDeep(
+        result.current,
+        getResult(mutationCache, optionsRef.current),
+      )
+      if (result.current !== nextResult) {
+        result.current = nextResult
+      }
+
+      return result.current
+    },
     () => result.current,
   )!
 }

From a1647943fe344ddf6c11e72d79a40ccca317ed8e Mon Sep 17 00:00:00 2001
From: Dominik Dorfmeister <office@dorfmeister.cc>
Date: Sat, 27 Jan 2024 14:45:52 +0100
Subject: [PATCH 5/6] chore: prettier

---
 packages/react-query/src/__tests__/useMutationState.test.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/react-query/src/__tests__/useMutationState.test.tsx b/packages/react-query/src/__tests__/useMutationState.test.tsx
index 643ced0c2a..05261e1554 100644
--- a/packages/react-query/src/__tests__/useMutationState.test.tsx
+++ b/packages/react-query/src/__tests__/useMutationState.test.tsx
@@ -116,7 +116,7 @@ describe('useIsMutating', () => {
       return null
     }
 
-    const IsMutating = React.memo(IsMutatingBase);
+    const IsMutating = React.memo(IsMutatingBase)
 
     function Page() {
       const { mutate: mutate1 } = useMutation({

From 7bf9b4e646d1d47ff2d386c38a317d2bbfd0050f Mon Sep 17 00:00:00 2001
From: Dominik Dorfmeister <office@dorfmeister.cc>
Date: Sat, 27 Jan 2024 14:48:07 +0100
Subject: [PATCH 6/6] test: revert timeout changes

---
 packages/react-query/src/__tests__/useIsFetching.test.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/react-query/src/__tests__/useIsFetching.test.tsx b/packages/react-query/src/__tests__/useIsFetching.test.tsx
index aec5b027ee..fe47628a4b 100644
--- a/packages/react-query/src/__tests__/useIsFetching.test.tsx
+++ b/packages/react-query/src/__tests__/useIsFetching.test.tsx
@@ -180,7 +180,7 @@ describe('useIsFetching', () => {
       useQuery({
         queryKey: key,
         queryFn: async () => {
-          await sleep(100)
+          await sleep(10)
           return 'test'
         },
       })