Repository: tannerlinsley/react-query
Pull request #1988 (2021-03-18 17:21:42)

docs: fix typo

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



Patch:
diff --git a/docs/src/manifests/manifest.json b/docs/src/manifests/manifest.json
index 9d436295d..84c03c719 100644
--- a/docs/src/manifests/manifest.json
+++ b/docs/src/manifests/manifest.json
@@ -166,9 +166,9 @@
           "editUrl": "/guides/scroll-restoration.md"
         },
         {
-          "title": "Query Filters",
-          "path": "/guides/query-filters",
-          "editUrl": "/guides/query-filters.md"
+          "title": "Filters",
+          "path": "/guides/filters",
+          "editUrl": "/guides/filters.md"
         },
         {
           "title": "SSR & Next.js",
@@ -348,6 +348,11 @@
           "path": "/reference/useIsFetching",
           "editUrl": "/reference/useIsFetching.md"
         },
+        {
+          "title": "useIsMutating",
+          "path": "/reference/useIsMutating",
+          "editUrl": "/reference/useIsMutating.md"
+        },
         {
           "title": "QueryClient",
           "path": "/reference/QueryClient",
diff --git a/docs/src/pages/guides/query-filters.md b/docs/src/pages/guides/filters.md
similarity index 54%
rename from docs/src/pages/guides/query-filters.md
rename to docs/src/pages/guides/filters.md
index df20c61b5..132decaf9 100644
--- a/docs/src/pages/guides/query-filters.md
+++ b/docs/src/pages/guides/filters.md
@@ -1,9 +1,13 @@
 ---
-id: query-filters
-title: Query Filters
+id: filters
+title: Filters
 ---
 
-Some methods within React Query accept a `QueryFilters` object. A query filter is an object with certain conditions to match a query with:
+Some methods within React Query accept a `QueryFilters` or `MutationFilters` object.
+
+## `Query Filters`
+
+A query filter is an object with certain conditions to match a query with:
 
 ```js
 // Cancel all queries
@@ -39,3 +43,30 @@ A query filter object supports the following properties:
   - This predicate function will be called for every single query in the cache and be expected to return truthy for queries that are `found`.
 - `queryKey?: QueryKey`
   - Set this property to define a query key to match on.
+
+## `Mutation Filters`
+
+A mutation filter is an object with certain conditions to match a mutation with:
+
+```js
+// Get the number of all fetching mutations
+await queryClient.isMutating()
+
+// Filter mutations by mutationKey
+await queryClient.isMutating({ mutationKey: "post" })
+
+// Filter mutations using a predicate function
+await queryClient.isMutating({ predicate: (mutation) => mutation.options.variables?.id === 1 })
+```
+
+A mutation filter object supports the following properties:
+
+- `exact?: boolean`
+  - If you don't want to search mutations inclusively by mutation key, you can pass the `exact: true` option to return only the mutation with the exact mutation key you have passed.
+- `fetching?: boolean`
+  - When set to `true` it will match mutations that are currently fetching.
+  - When set to `false` it will match mutations that are not fetching.
+- `predicate?: (mutation: Mutation) => boolean`
+  - This predicate function will be called for every single mutation in the cache and be expected to return truthy for mutations that are `found`.
+- `mutationKey?: MutationKey`
+  - Set this property to define a mutation key to match on.
diff --git a/docs/src/pages/guides/query-invalidation.md b/docs/src/pages/guides/query-invalidation.md
index af2d31959..2f6b2b678 100644
--- a/docs/src/pages/guides/query-invalidation.md
+++ b/docs/src/pages/guides/query-invalidation.md
@@ -21,7 +21,7 @@ When a query is invalidated with `invalidateQueries`, two things happen:
 
 ## Query Matching with `invalidateQueries`
 
-When using APIs like `invalidateQueries` and `removeQueries` (and others that support partial query matching), you can match multiple queries by their prefix, or get really specific and match an exact query. For information on the types of filters you can use, please see [Query Filters](./query-filters).
+When using APIs like `invalidateQueries` and `removeQueries` (and others that support partial query matching), you can match multiple queries by their prefix, or get really specific and match an exact query. For information on the types of filters you can use, please see [Query Filters](./filters#query-filters).
 
 In this example, we can use the `todos` prefix to invalidate any queries that start with `todos` in their query key:
 
diff --git a/docs/src/pages/guides/ssr.md b/docs/src/pages/guides/ssr.md
index 1e3ea5578..0822bb7b1 100644
--- a/docs/src/pages/guides/ssr.md
+++ b/docs/src/pages/guides/ssr.md
@@ -116,7 +116,7 @@ As demonstrated, it's fine to prefetch some queries and let others fetch on the
 
 This guide is at-best, a high level overview of how SSR with React Query should work. Your mileage may vary since there are many different possible setups for SSR.
 
-> If you can, please contribution your findings back to this page for any framework specific guidance!
+> If you can, please contribute your findings back to this page for any framework specific guidance!
 
 ### On the Server
 
diff --git a/docs/src/pages/reference/QueryCache.md b/docs/src/pages/reference/QueryCache.md
index 3e26d55c2..f575eb332 100644
--- a/docs/src/pages/reference/QueryCache.md
+++ b/docs/src/pages/reference/QueryCache.md
@@ -45,7 +45,7 @@ const query = queryCache.find(queryKey)
 **Options**
 
 - `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
-- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)
+- `filters?: QueryFilters`: [Query Filters](../guides/filters#query-filters)
 
 **Returns**
 
@@ -65,7 +65,7 @@ const queries = queryCache.findAll(queryKey)
 **Options**
 
 - `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
-- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)
+- `filters?: QueryFilters`: [Query Filters](../guides/filters#query-filters)
 
 **Returns**
 
diff --git a/docs/src/pages/reference/QueryClient.md b/docs/src/pages/reference/QueryClient.md
index cd3d7e340..e7d885a47 100644
--- a/docs/src/pages/reference/QueryClient.md
+++ b/docs/src/pages/reference/QueryClient.md
@@ -36,6 +36,7 @@ Its available methods are:
 - [`removeQueries`](#queryclientremovequeries)
 - [`resetQueries`](#queryclientresetqueries)
 - [`isFetching`](#queryclientisfetching)
+- [`isMutating`](#queryclientismutating)
 - [`getDefaultOptions`](#queryclientsetdefaultoptions)
 - [`setDefaultOptions`](#queryclientgetdefaultoptions)
 - [`getQueryDefaults`](#queryclientgetquerydefaults)
@@ -166,7 +167,7 @@ const data = queryClient.getQueryData(queryKey)
 **Options**
 
 - `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
-- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)
+- `filters?: QueryFilters`: [Query Filters](../guides/filters#query-filters)
 
 **Returns**
 
@@ -216,7 +217,7 @@ console.log(state.dataUpdatedAt)
 **Options**
 
 - `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
-- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)
+- `filters?: QueryFilters`: [Query Filters](../guides/filters#query-filters)
 
 ## `queryClient.invalidateQueries`
 
@@ -236,7 +237,7 @@ await queryClient.invalidateQueries('posts', {
 **Options**
 
 - `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
-- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)
+- `filters?: QueryFilters`: [Query Filters](../guides/filters#query-filters)
   - `refetchActive: Boolean`
     - Defaults to `true`
     - When set to `false`, queries that match the refetch predicate and are actively being rendered via `useQuery` and friends will NOT be refetched in the background, and only marked as invalid.
@@ -270,7 +271,7 @@ await queryClient.refetchQueries(['posts', 1], { active: true, exact: true })
 **Options**
 
 - `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
-- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)
+- `filters?: QueryFilters`: [Query Filters](../guides/filters#query-filters)
 - `refetchOptions?: RefetchOptions`:
   - `throwOnError?: boolean`
     - When set to `true`, this method will throw if any of the query refetch tasks fail.
@@ -292,7 +293,7 @@ await queryClient.cancelQueries('posts', { exact: true })
 **Options**
 
 - `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
-- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)
+- `filters?: QueryFilters`: [Query Filters](../guides/filters#query-filters)
 
 **Returns**
 
@@ -309,7 +310,7 @@ queryClient.removeQueries(queryKey, { exact: true })
 **Options**
 
 - `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
-- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)
+- `filters?: QueryFilters`: [Query Filters](../guides/filters#query-filters)
 
 **Returns**
 
@@ -333,7 +334,7 @@ queryClient.resetQueries(queryKey, { exact: true })
 **Options**
 
 - `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
-- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)
+- `filters?: QueryFilters`: [Query Filters](../guides/filters#query-filters)
 - `resetOptions?: ResetOptions`:
   - `throwOnError?: boolean`
     - When set to `true`, this method will throw if any of the query refetch tasks fail.
@@ -352,17 +353,36 @@ if (queryClient.isFetching()) {
 }
 ```
 
-React Query also exports a handy [`useIsFetching`](#useisfetching) hook that will let you subscribe to this state in your components without creating a manual subscription to the query cache.
+React Query also exports a handy [`useIsFetching`](./useIsFetching) hook that will let you subscribe to this state in your components without creating a manual subscription to the query cache.
 
 **Options**
 
 - `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
-- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)
+- `filters?: QueryFilters`: [Query Filters](../guides/filters#query-filters)
 
 **Returns**
 
 This method returns the number of fetching queries.
 
+## `queryClient.isMutating`
+
+This `isMutating` method returns an `integer` representing how many mutations, if any, in the cache are currently fetching.
+
+```js
+if (queryClient.isMutating()) {
+  console.log('At least one mutation is fetching!')
+}
+```
+
+React Query also exports a handy [`useIsMutating`](./useIsMutating) hook that will let you subscribe to this state in your components without creating a manual subscription to the mutation cache.
+
+**Options**
+
+- `filters: MutationFilters`: [Mutation Filters](../guides/filters#mutation-filters)
+
+**Returns**
+
+This method returns the number of fetching mutations.
 ## `queryClient.getDefaultOptions`
 
 The `getDefaultOptions` method returns the default options which have been set when creating the client or with `setDefaultOptions`.
diff --git a/docs/src/pages/reference/useIsFetching.md b/docs/src/pages/reference/useIsFetching.md
index 20ce78e28..569c13344 100644
--- a/docs/src/pages/reference/useIsFetching.md
+++ b/docs/src/pages/reference/useIsFetching.md
@@ -16,7 +16,7 @@ const isFetchingPosts = useIsFetching(['posts'])
 **Options**
 
 - `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
-- `filters?: QueryFilters`: [Query Filters](../guides/query-filters)
+- `filters?: QueryFilters`: [Query Filters](../guides/filters#query-filters)
 
 **Returns**
 
diff --git a/docs/src/pages/reference/useIsMutating.md b/docs/src/pages/reference/useIsMutating.md
new file mode 100644
index 000000000..93e323345
--- /dev/null
+++ b/docs/src/pages/reference/useIsMutating.md
@@ -0,0 +1,23 @@
+---
+id: useIsMutating
+title: useIsMutating
+---
+
+`useIsMutating` is an optional hook that returns the `number` of mutations that your application is fetching (useful for app-wide loading indicators).
+
+```js
+import { useIsMutating } from 'react-query'
+// How many mutations are fetching?
+const isMutating = useIsMutating()
+// How many mutations matching the posts prefix are fetching?
+const isMutatingPosts = useIsMutating(['posts'])
+```
+
+**Options**
+
+- `filters?: MutationFilters`: [Mutation Filters](../guides/filters#mutation-filters)
+
+**Returns**
+
+- `isMutating: number`
+  - Will be the `number` of the mutations that your application is currently fetching.
diff --git a/src/core/mutationCache.ts b/src/core/mutationCache.ts
index 1ed431750..d1649d9c3 100644
--- a/src/core/mutationCache.ts
+++ b/src/core/mutationCache.ts
@@ -2,7 +2,7 @@ import type { MutationOptions } from './types'
 import type { QueryClient } from './queryClient'
 import { notifyManager } from './notifyManager'
 import { Mutation, MutationState } from './mutation'
-import { noop } from './utils'
+import { matchMutation, MutationFilters, noop } from './utils'
 import { Subscribable } from './subscribable'
 
 // TYPES
@@ -76,6 +76,25 @@ export class MutationCache extends Subscribable<MutationCacheListener> {
     return this.mutations
   }
 
+  find<
+    TData = unknown,
+    TError = unknown,
+    TVariables = any,
+    TContext = unknown
+  >(
+    filters: MutationFilters
+  ): Mutation<TData, TError, TVariables, TContext> | undefined {
+    if (typeof filters.exact === 'undefined') {
+      filters.exact = true
+    }
+
+    return this.mutations.find(mutation => matchMutation(filters, mutation))
+  }
+
+  findAll(filters: MutationFilters): Mutation[] {
+    return this.mutations.filter(mutation => matchMutation(filters, mutation))
+  }
+
   notify(mutation?: Mutation<any, any, any, any>) {
     notifyManager.batch(() => {
       this.listeners.forEach(listener => {
diff --git a/src/core/queryClient.ts b/src/core/queryClient.ts
index cc7c5427b..85f47cf54 100644
--- a/src/core/queryClient.ts
+++ b/src/core/queryClient.ts
@@ -7,6 +7,7 @@ import {
   parseQueryArgs,
   partialMatchKey,
   hashQueryKeyByOptions,
+  MutationFilters,
 } from './utils'
 import type {
   DefaultOptions,
@@ -99,6 +100,10 @@ export class QueryClient {
     return this.queryCache.findAll(filters).length
   }
 
+  isMutating(filters?: MutationFilters): number {
+    return this.mutationCache.findAll({ ...filters, fetching: true }).length
+  }
+
   getQueryData<TData = unknown>(
     queryKey: QueryKey,
     filters?: QueryFilters
diff --git a/src/core/utils.ts b/src/core/utils.ts
index 49cb0b7d6..5c643299d 100644
--- a/src/core/utils.ts
+++ b/src/core/utils.ts
@@ -1,3 +1,4 @@
+import type { Mutation } from './mutation'
 import type { Query } from './query'
 import type {
   MutationFunction,
@@ -41,6 +42,25 @@ export interface QueryFilters {
   fetching?: boolean
 }
 
+export interface MutationFilters {
+  /**
+   * Match mutation key exactly
+   */
+  exact?: boolean
+  /**
+   * Include mutations matching this predicate function
+   */
+  predicate?: (mutation: Mutation<any, any, any>) => boolean
+  /**
+   * Include mutations matching this mutation key
+   */
+  mutationKey?: MutationKey
+  /**
+   * Include or exclude fetching mutations
+   */
+  fetching?: boolean
+}
+
 export type DataUpdateFunction<TInput, TOutput> = (input: TInput) => TOutput
 
 export type Updater<TInput, TOutput> =
@@ -187,6 +207,40 @@ export function matchQuery(
   return true
 }
 
+export function matchMutation(
+  filters: MutationFilters,
+  mutation: Mutation<any, any>
+): boolean {
+  const { exact, fetching, predicate, mutationKey } = filters
+  if (isQueryKey(mutationKey)) {
+    if (!mutation.options.mutationKey) {
+      return false
+    }
+    if (exact) {
+      if (
+        hashQueryKey(mutation.options.mutationKey) !== hashQueryKey(mutationKey)
+      ) {
+        return false
+      }
+    } else if (!partialMatchKey(mutation.options.mutationKey, mutationKey)) {
+      return false
+    }
+  }
+
+  if (
+    typeof fetching === 'boolean' &&
+    (mutation.state.status === 'loading') !== fetching
+  ) {
+    return false
+  }
+
+  if (predicate && !predicate(mutation)) {
+    return false
+  }
+
+  return true
+}
+
 export function hashQueryKeyByOptions(
   queryKey: QueryKey,
   options?: QueryOptions<any, any>
@@ -222,10 +276,7 @@ export function stableValueHash(value: any): string {
 /**
  * Checks if key `b` partially matches with key `a`.
  */
-export function partialMatchKey(
-  a: QueryKey,
-  b: QueryKey
-): boolean {
+export function partialMatchKey(a: QueryKey, b: QueryKey): boolean {
   return partialDeepEqual(ensureArray(a), ensureArray(b))
 }
 
diff --git a/src/react/index.ts b/src/react/index.ts
index e9245345e..512d6d8e9 100644
--- a/src/react/index.ts
+++ b/src/react/index.ts
@@ -8,6 +8,7 @@ export {
   useQueryErrorResetBoundary,
 } from './QueryErrorResetBoundary'
 export { useIsFetching } from './useIsFetching'
+export { useIsMutating } from './useIsMutating'
 export { useMutation } from './useMutation'
 export { useQuery } from './useQuery'
 export { useQueries } from './useQueries'
diff --git a/src/react/useIsMutating.ts b/src/react/useIsMutating.ts
new file mode 100644
index 000000000..3f6987be2
--- /dev/null
+++ b/src/react/useIsMutating.ts
@@ -0,0 +1,42 @@
+import React from 'react'
+
+import { notifyManager } from '../core/notifyManager'
+import { MutationFilters } from '../core/utils'
+import { useQueryClient } from './QueryClientProvider'
+
+export function useIsMutating(filters?: MutationFilters): number {
+  const mountedRef = React.useRef(false)
+
+  const queryClient = useQueryClient()
+
+  const [isMutating, setIsMutating] = React.useState(
+    queryClient.isMutating(filters)
+  )
+
+  const filtersRef = React.useRef(filters)
+  filtersRef.current = filters
+  const isMutatingRef = React.useRef(isMutating)
+  isMutatingRef.current = isMutating
+
+  React.useEffect(() => {
+    mountedRef.current = true
+
+    const unsubscribe = queryClient.getMutationCache().subscribe(
+      notifyManager.batchCalls(() => {
+        if (mountedRef.current) {
+          const newIsMutating = queryClient.isMutating(filtersRef.current)
+          if (isMutatingRef.current !== newIsMutating) {
+            setIsMutating(newIsMutating)
+          }
+        }
+      })
+    )
+
+    return () => {
+      mountedRef.current = false
+      unsubscribe()
+    }
+  }, [queryClient])
+
+  return isMutating
+}


Test patch:
diff --git a/src/core/tests/mutationCache.test.tsx b/src/core/tests/mutationCache.test.tsx
index 9705b2228..c1d1135f2 100644
--- a/src/core/tests/mutationCache.test.tsx
+++ b/src/core/tests/mutationCache.test.tsx
@@ -25,4 +25,57 @@ describe('mutationCache', () => {
       expect(onError).toHaveBeenCalledWith('error', 'vars', 'context', mutation)
     })
   })
+
+  describe('find', () => {
+    test('should filter correctly', async () => {
+      const testCache = new MutationCache()
+      const testClient = new QueryClient({ mutationCache: testCache })
+      const key = ['mutation', 'vars']
+      await testClient.executeMutation({
+        mutationKey: key,
+        variables: 'vars',
+        mutationFn: () => Promise.resolve(),
+      })
+      const [mutation] = testCache.getAll()
+      expect(testCache.find({ mutationKey: key })).toEqual(mutation)
+      expect(testCache.find({ mutationKey: 'mutation', exact: false })).toEqual(
+        mutation
+      )
+      expect(testCache.find({ mutationKey: 'unknown' })).toEqual(undefined)
+      expect(
+        testCache.find({ predicate: m => m.options.variables === 'vars' })
+      ).toEqual(mutation)
+    })
+  })
+
+  describe('findAll', () => {
+    test('should filter correctly', async () => {
+      const testCache = new MutationCache()
+      const testClient = new QueryClient({ mutationCache: testCache })
+      await testClient.executeMutation({
+        mutationKey: ['a', 1],
+        variables: 1,
+        mutationFn: () => Promise.resolve(),
+      })
+      await testClient.executeMutation({
+        mutationKey: ['a', 2],
+        variables: 2,
+        mutationFn: () => Promise.resolve(),
+      })
+      await testClient.executeMutation({
+        mutationKey: 'b',
+        mutationFn: () => Promise.resolve(),
+      })
+
+      const [mutation1, mutation2] = testCache.getAll()
+      expect(
+        testCache.findAll({ mutationKey: 'a', exact: false })
+      ).toHaveLength(2)
+      expect(testCache.find({ mutationKey: ['a', 1] })).toEqual(mutation1)
+      expect(testCache.findAll({ mutationKey: 'unknown' })).toEqual([])
+      expect(
+        testCache.findAll({ predicate: m => m.options.variables === 2 })
+      ).toEqual([mutation2])
+    })
+  })
 })
diff --git a/src/react/tests/useIsMutating.test.tsx b/src/react/tests/useIsMutating.test.tsx
new file mode 100644
index 000000000..2b1f5b644
--- /dev/null
+++ b/src/react/tests/useIsMutating.test.tsx
@@ -0,0 +1,108 @@
+import { waitFor } from '@testing-library/react'
+import React from 'react'
+import { QueryClient } from '../../core'
+import { useIsMutating } from '../useIsMutating'
+import { useMutation } from '../useMutation'
+import { renderWithClient, setActTimeout, sleep } from './utils'
+
+describe('useIsMutating', () => {
+  it('should return the number of fetching mutations', async () => {
+    const isMutatings: number[] = []
+    const queryClient = new QueryClient()
+
+    function IsMutating() {
+      const isMutating = useIsMutating()
+      isMutatings.push(isMutating)
+      return null
+    }
+
+    function Page() {
+      const { mutate: mutate1 } = useMutation('mutation1', async () => {
+        await sleep(150)
+        return 'data'
+      })
+      const { mutate: mutate2 } = useMutation('mutation2', async () => {
+        await sleep(50)
+        return 'data'
+      })
+
+      React.useEffect(() => {
+        mutate1()
+        setActTimeout(() => {
+          mutate2()
+        }, 50)
+      }, [mutate1, mutate2])
+
+      return <IsMutating />
+    }
+
+    renderWithClient(queryClient, <Page />)
+    await waitFor(() => expect(isMutatings).toEqual([0, 1, 1, 2, 2, 1, 0]))
+  })
+
+  it('should filter correctly by mutationKey', async () => {
+    const isMutatings: number[] = []
+    const queryClient = new QueryClient()
+
+    function IsMutating() {
+      const isMutating = useIsMutating({ mutationKey: 'mutation1' })
+      isMutatings.push(isMutating)
+      return null
+    }
+
+    function Page() {
+      const { mutate: mutate1 } = useMutation('mutation1', async () => {
+        await sleep(100)
+        return 'data'
+      })
+      const { mutate: mutate2 } = useMutation('mutation2', async () => {
+        await sleep(100)
+        return 'data'
+      })
+
+      React.useEffect(() => {
+        mutate1()
+        mutate2()
+      }, [mutate1, mutate2])
+
+      return <IsMutating />
+    }
+
+    renderWithClient(queryClient, <Page />)
+    await waitFor(() => expect(isMutatings).toEqual([0, 1, 1, 1, 0, 0]))
+  })
+
+  it('should filter correctly by predicate', async () => {
+    const isMutatings: number[] = []
+    const queryClient = new QueryClient()
+
+    function IsMutating() {
+      const isMutating = useIsMutating({
+        predicate: mutation => mutation.options.mutationKey === 'mutation1',
+      })
+      isMutatings.push(isMutating)
+      return null
+    }
+
+    function Page() {
+      const { mutate: mutate1 } = useMutation('mutation1', async () => {
+        await sleep(100)
+        return 'data'
+      })
+      const { mutate: mutate2 } = useMutation('mutation2', async () => {
+        await sleep(100)
+        return 'data'
+      })
+
+      React.useEffect(() => {
+        mutate1()
+        mutate2()
+      }, [mutate1, mutate2])
+
+      return <IsMutating />
+    }
+
+    renderWithClient(queryClient, <Page />)
+    await waitFor(() => expect(isMutatings).toEqual([0, 1, 1, 1, 0, 0]))
+  })
+})