Repository: tannerlinsley/react-query
Pull request #3038 (2021-11-30 19:30:26)

Add `onMutate` to `MutationCache`
following up on https://github.com/tannerlinsley/react-query/discussions/3035 this pr allows adding a `onMutate` callback to the global `MutationCache`. this enables e.g. managing toast notifications for mutations in one central place (see example below). note that it is not allowed to return a context value from the `onMutate` callback.

example:
```js
import { MutationCache, QueryClient, QueryClientProvider } from "react-query";
import { Toaster, toast } from "react-hot-toast";
import { useState } from "react";

export default function App({ Component, pageProps }) {
  const [client] = useState(() => {
    return new QueryClient({
      mutationCache: new MutationCache({
        onMutate(variables, mutation) {
          toast.loading("Submitting...", { id: mutation.mutationId });
        },
        onSuccess(data, variables, context, mutation) {
          toast.success("Success", { id: mutation.mutationId });
        },
        onError(error, variables, context, mutation) {
          toast.error("Failed", { id: mutation.mutationId });
        },
      }),
    });
  });

  return (
    <QueryClientProvider client={client}>
      <Component {...pageProps} />
      <Toaster />
    </QueryClientProvider>
  );
}
```

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


codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/tannerlinsley/react-query/pr/3038/builds/193686) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 8182702c5da164f75e76da39164c5a6fe1951647:

|Sandbox| Source |
|--|--|
|[tannerlinsley/react-query: basic](https://codesandbox.io/s/tannerlinsley-react-query-basic-sox13)| Configuration |
|[tannerlinsley/react-query](https://codesandbox.io/s/tannerlinsley-react-query-1unte)| Configuration |

codecov[bot]: # [Codecov](https://codecov.io/gh/tannerlinsley/react-query/pull/3038?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) Report
> Merging [#3038](https://codecov.io/gh/tannerlinsley/react-query/pull/3038?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) (8182702) into [master](https://codecov.io/gh/tannerlinsley/react-query/commit/dc2df1066fb35593ab9f7b852ee250424f212737?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) (dc2df10) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/tannerlinsley/react-query/pull/3038/graphs/tree.svg?width=650&height=150&src=pr&token=jqEbswkEDQ&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley)](https://codecov.io/gh/tannerlinsley/react-query/pull/3038?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley)

```diff
@@           Coverage Diff           @@
##           master    #3038   +/-   ##
=======================================
  Coverage   96.45%   96.45%           
=======================================
  Files          45       45           
  Lines        2257     2259    +2     
  Branches      641      641           
=======================================
+ Hits         2177     2179    +2     
  Misses         77       77           
  Partials        3        3           
```


| [Impacted Files](https://codecov.io/gh/tannerlinsley/react-query/pull/3038?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) | Coverage Δ | |
|---|---|---|
| [src/core/mutationCache.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3038/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvbXV0YXRpb25DYWNoZS50cw==) | `100.00% <ø> (ø)` | |
| [src/core/mutation.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3038/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvbXV0YXRpb24udHM=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tannerlinsley/react-query/pull/3038?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tannerlinsley/react-query/pull/3038?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley). Last update [dc2df10...8182702](https://codecov.io/gh/tannerlinsley/react-query/pull/3038?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley).


Patch:
diff --git a/docs/src/pages/reference/MutationCache.md b/docs/src/pages/reference/MutationCache.md
index 8bb76b474..06f1d52e9 100644
--- a/docs/src/pages/reference/MutationCache.md
+++ b/docs/src/pages/reference/MutationCache.md
@@ -16,7 +16,7 @@ const mutationCache = new MutationCache({
   },
   onSuccess: data => {
     console.log(data)
-  }
+  },
 })
 ```
 
@@ -34,11 +34,16 @@ Its available methods are:
 - `onSuccess?: (data: unknown, variables: unknown, context: unknown, mutation: Mutation) => void`
   - Optional
   - This function will be called if some mutation is successful.
+- `onMutate?: (variables: unknown, mutation: Mutation) => void`
+  - Optional
+  - This function will be called before some mutation executes.
 
 ## Global callbacks
 
-The `onError` and `onSuccess` callbacks on the MutationCache can be used to handle these events on a global level. They are different to `defaultOptions` provided to the QueryClient because:
+The `onError`, `onSuccess` and `onMutate` callbacks on the MutationCache can be used to handle these events on a global level. They are different to `defaultOptions` provided to the QueryClient because:
+
 - `defaultOptions` can be overridden by each Mutation - the global callbacks will **always** be called.
+- `onMutate` does not allow returning a context value.
 
 ## `mutationCache.getAll`
 
diff --git a/src/core/mutation.ts b/src/core/mutation.ts
index 3846e60ba..58796a3a6 100644
--- a/src/core/mutation.ts
+++ b/src/core/mutation.ts
@@ -143,6 +143,13 @@ export class Mutation<
     if (!restored) {
       this.dispatch({ type: 'loading', variables: this.options.variables! })
       promise = promise
+        .then(() => {
+          // Notify cache callback
+          this.mutationCache.config.onMutate?.(
+            this.state.variables,
+            this as Mutation<unknown, unknown, unknown, unknown>
+          )
+        })
         .then(() => this.options.onMutate?.(this.state.variables!))
         .then(context => {
           if (context !== this.state.context) {
diff --git a/src/core/mutationCache.ts b/src/core/mutationCache.ts
index eef98e3ec..624e6972a 100644
--- a/src/core/mutationCache.ts
+++ b/src/core/mutationCache.ts
@@ -20,6 +20,10 @@ interface MutationCacheConfig {
     context: unknown,
     mutation: Mutation<unknown, unknown, unknown, unknown>
   ) => void
+  onMutate?: (
+    variables: unknown,
+    mutation: Mutation<unknown, unknown, unknown, unknown>
+  ) => void
 }
 
 type MutationCacheListener = (mutation?: Mutation) => void


Test patch:
diff --git a/src/core/tests/mutationCache.test.tsx b/src/core/tests/mutationCache.test.tsx
index 425c43890..c0bd84f48 100644
--- a/src/core/tests/mutationCache.test.tsx
+++ b/src/core/tests/mutationCache.test.tsx
@@ -53,6 +53,29 @@ describe('mutationCache', () => {
       )
     })
   })
+  describe('MutationCacheConfig.onMutate', () => {
+    test('should be called before a mutation executes', async () => {
+      const consoleMock = mockConsoleError()
+      const key = queryKey()
+      const onMutate = jest.fn()
+      const testCache = new MutationCache({ onMutate })
+      const testClient = new QueryClient({ mutationCache: testCache })
+
+      try {
+        await testClient.executeMutation({
+          mutationKey: key,
+          variables: 'vars',
+          mutationFn: () => Promise.resolve({ data: 5 }),
+          onMutate: () => 'context',
+        })
+      } catch {
+        consoleMock.mockRestore()
+      }
+
+      const mutation = testCache.getAll()[0]
+      expect(onMutate).toHaveBeenCalledWith('vars', mutation)
+    })
+  })
 
   describe('find', () => {
     test('should filter correctly', async () => {