Repository: tannerlinsley/react-query
Pull request #1130 (2020-10-04 10:22:28)

feat: split mutate into mutate and mutateAsync
Currently the `mutate` function returns a promise which by default returns `undefined` on error. This behavior seems to confuse some people as they initially expect the promise to behave like a regular promise. There is a `throwOnError` option to make the promise behave like a regular promise, but this option seems to be overlooked and does not work well with the types. Returning a different type based on an option is possible (although a bit cumbersome), but it's impossible to guarantee the correctness because it can also be defined globally. This PR splits the functionality into `mutate` and `mutateAsync`. The `mutate` function returns nothing and the `mutateAsync` function returns a regular promise.

Issue https://github.com/tannerlinsley/react-query/issues/1111

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/7n8islmji](https://vercel.com/tannerlinsley/react-query/7n8islmji)  
✅ Preview: [https://react-query-git-feature-split-use-mutation.tannerlinsley.vercel.app](https://react-query-git-feature-split-use-mutation.tannerlinsley.vercel.app)


tannerlinsley: Sounds like we should deprecate the original and switch to the async version in v3?
tannerlinsley: Not a fan of the combined returned variable. The naming of the actual function is nice as opposed to requiring the use of thing.mutate() syntax
boschni: The downside of returning a regular promise by default is that it would require users to always wrap the mutate function:
```js
const [mutate] = useMutation()
return (
  <button onClick={async () => {
    try {
      await mutate()
    } catch {}
  }} />
)
```
Instead of:
```js
const [mutate] = useMutation()
return (
  <button onClick={mutate} />
)
```
Was also a bit in doubt on the object return value, but it does align more with `useQuery` and destructuring is still possible:
```js
const { mutate } = useMutation()
return (
  <button onClick={mutate} />
)
```
Or:
```js
const { mutate: addTodo } = useMutation()
return (
  <button onClick={addTodo} />
)
```

Patch:
diff --git a/docs/src/pages/guides/invalidations-from-mutations.md b/docs/src/pages/guides/invalidations-from-mutations.md
index 30ee13aac..81a7f4e1f 100644
--- a/docs/src/pages/guides/invalidations-from-mutations.md
+++ b/docs/src/pages/guides/invalidations-from-mutations.md
@@ -8,7 +8,7 @@ Invalidating queries is only half the battle. Knowing **when** to invalidate the
 For example, assume we have a mutation to post a new todo:
 
 ```js
-const [mutate] = useMutation(postTodo)
+const mutation = useMutation(postTodo)
 ```
 
 When a successful `postTodo` mutation happens, we likely want all `todos` queries to get invalidated and possibly refetched to show the new todo item. To do this, you can use `useMutation`'s `onSuccess` options and the `client`'s `invalidateQueries` function:
@@ -19,7 +19,7 @@ import { useMutation, useQueryClient } from 'react-query'
 const client = useQueryClient()
 
 // When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key
-const [mutate] = useMutation(addTodo, {
+const mutation = useMutation(addTodo, {
   onSuccess: () => {
     client.invalidateQueries('todos')
     client.invalidateQueries('reminders')
diff --git a/docs/src/pages/guides/mutations.md b/docs/src/pages/guides/mutations.md
index 38005b119..07e138d3f 100644
--- a/docs/src/pages/guides/mutations.md
+++ b/docs/src/pages/guides/mutations.md
@@ -9,22 +9,25 @@ Here's an example of a mutation that adds a new todo the server:
 
 ```js
 function App() {
-  const [
-    mutate,
-    { isLoading, isError, isSuccess, data, error },
-  ] = useMutation(newTodo => axios.post('/todods', newTodo))
+  const mutation = useMutation(newTodo => axios.post('/todods', newTodo))
 
   return (
     <div>
-      {isLoading ? (
+      {mutation.isLoading ? (
         'Adding todo...'
       ) : (
         <>
-          {isError ? <div>An error occurred: {error.message}</div> : null}
+          {mutation.isError ? (
+            <div>An error occurred: {mutation.error.message}</div>
+          ) : null}
 
-          {isError ? <div>Todo added!</div> : null}
+          {mutation.isError ? <div>Todo added!</div> : null}
 
-          <button onClick={mutate({ id: new Date(), title: 'Do Laundry' })}>
+          <button
+            onClick={() => {
+              mutation.mutate({ id: new Date(), title: 'Do Laundry' })
+            }}
+          >
             Create Todo
           </button>
         </>
@@ -55,22 +58,22 @@ Even with just variables, mutations aren't all that special, but when used with
 ```js
 // This will not work
 const CreateTodo = () => {
-  const [mutate] = useMutation(event => {
+  const mutation = useMutation(event => {
     event.preventDefault()
     return fetch('/api', new FormData(event.target))
   })
 
-  return <form onSubmit={mutate}>...</form>
+  return <form onSubmit={mutation.mutate}>...</form>
 }
 
 // This will work
 const CreateTodo = () => {
-  const [mutate] = useMutation(formData => {
+  const mutation = useMutation(formData => {
     return fetch('/api', formData)
   })
   const onSubmit = event => {
     event.preventDefault()
-    mutate(new FormData(event.target))
+    mutation.mutate(new FormData(event.target))
   }
 
   return <form onSubmit={onSubmit}>...</form>
@@ -84,16 +87,18 @@ It's sometimes the case that you need to clear the `error` or `data` of a mutati
 ```js
 const CreateTodo = () => {
   const [title, setTitle] = useState('')
-  const [mutate, { error, reset }] = useMutation(createTodo)
+  const mutation = useMutation(createTodo)
 
-  const onCreateTodo = async e => {
+  const onCreateTodo = e => {
     e.preventDefault()
-    await mutate({ title })
+    mutation.mutate({ title })
   }
 
   return (
     <form onSubmit={onCreateTodo}>
-      {error && <h5 onClick={() => reset()}>{error}</h5>}
+      {mutation.error && (
+        <h5 onClick={() => mutation.reset()}>{mutation.error}</h5>
+      )}
       <input
         type="text"
         value={title}
@@ -111,32 +116,36 @@ const CreateTodo = () => {
 `useMutation` comes with some helper options that allow quick and easy side-effects at any stage during the mutation lifecycle. These come in handy for both [invalidating and refetching queries after mutations](../invalidations-from-mutations) and even [optimistic updates](../optimistic-updates)
 
 ```js
-const [mutate] = useMutation(addTodo, {
-  onMutate: (variables) => {
+useMutation(addTodo, {
+  onMutate: variables => {
     // A mutation is about to happen!
 
-    // Optionally return a rollbackVariable
-    return () => {
-      // do some rollback logic
+    // Optionally return a context object with a rollback function
+    return {
+      rollback: () => {
+        // do some rollback logic
+      },
     }
-  }
-  onError: (error, variables, rollbackVariable) => {
+  },
+  onError: (error, variables, context) => {
     // An error happened!
-    if (rollbackVariable) rollbackVariable()
+    if (context.rollback) {
+      context.rollback()
+    }
   },
-  onSuccess: (data, variables, rollbackVariable) => {
+  onSuccess: (data, variables, context) => {
     // Boom baby!
   },
-  onSettled: (data, error, variables, rollbackVariable) => {
+  onSettled: (data, error, variables, context) => {
     // Error or success... doesn't matter!
   },
 })
 ```
 
-The promise returned by `mutate()` can be helpful as well for performing more granular control flow in your app, and if you prefer that that promise only resolves **after** the `onSuccess` or `onSettled` callbacks, you can return a promise in either!:
+When returning a promise in any of the callback functions it will first be awaited before the next callback is called:
 
 ```js
-const [mutate] = useMutation(addTodo, {
+useMutation(addTodo, {
   onSuccess: async () => {
     console.log("I'm first!")
   },
@@ -144,8 +153,6 @@ const [mutate] = useMutation(addTodo, {
     console.log("I'm second!")
   },
 })
-
-mutate(todo)
 ```
 
 You might find that you want to **add additional side-effects** to some of the `useMutation` lifecycle at the time of calling `mutate`. To do that, you can provide any of the same callback options to the `mutate` function after your mutation variable. Supported option overrides include:
@@ -153,31 +160,44 @@ You might find that you want to **add additional side-effects** to some of the `
 - `onSuccess` - Will be fired after the `useMutation`-level `onSuccess` handler
 - `onError` - Will be fired after the `useMutation`-level `onError` handler
 - `onSettled` - Will be fired after the `useMutation`-level `onSettled` handler
-- `throwOnError` - Indicates that the `mutate` function should throw an error if one is encountered
 
 ```js
-const [mutate] = useMutation(addTodo, {
-  onSuccess: (data, mutationVariables) => {
+useMutation(addTodo, {
+  onSuccess: (data, variables, context) => {
     // I will fire first
   },
-  onError: (error, mutationVariables) => {
+  onError: (error, variables, context) => {
     // I will fire first
   },
-  onSettled: (data, error, mutationVariables) => {
+  onSettled: (data, error, variables, context) => {
     // I will fire first
   },
 })
 
 mutate(todo, {
-  onSuccess: (data, mutationVariables) => {
+  onSuccess: (data, variables, context) => {
     // I will fire second!
   },
-  onError: (error, mutationVariables) => {
+  onError: (error, variables, context) => {
     // I will fire second!
   },
-  onSettled: (data, error, mutationVariables) => {
+  onSettled: (data, error, variables, context) => {
     // I will fire second!
   },
-  throwOnError: true,
 })
 ```
+
+## Promises
+
+Use `mutateAsync` instead of `mutate` to get a promise which will resolve on success or throw on an error:
+
+```js
+const mutation = useMutation(addTodo)
+
+try {
+  const todo = await mutation.mutateAsync(todo)
+  console.log(todo)
+} catch (error) {
+  console.error(error)
+}
+```
diff --git a/docs/src/pages/guides/queries.md b/docs/src/pages/guides/queries.md
index 2e088dda0..3d9a245c4 100644
--- a/docs/src/pages/guides/queries.md
+++ b/docs/src/pages/guides/queries.md
@@ -5,7 +5,7 @@ title: Queries
 
 ## Query Basics
 
-A query is a declarative dependency on an asynchronous source of data that is tied to a **unique key**
+A query is a declarative dependency on an asynchronous source of data that is tied to a **unique key**. A query can be used with any Promise based method (including GET and POST methods) to fetch data from a server. If your method modifies data on the server, we recommend using [Mutations](https://react-query.tanstack.com/docs/guides/mutations) instead.
 
 To subscribe to a query in your components or custom hooks, call the `useQuery` hook with at least:
 
diff --git a/docs/src/pages/guides/updates-from-mutation-responses.md b/docs/src/pages/guides/updates-from-mutation-responses.md
index 4abb10cbb..67493d274 100644
--- a/docs/src/pages/guides/updates-from-mutation-responses.md
+++ b/docs/src/pages/guides/updates-from-mutation-responses.md
@@ -8,11 +8,11 @@ When dealing with mutations that **update** objects on the server, it's common f
 ```js
 const client = useQueryClient()
 
-const [mutate] = useMutation(editTodo, {
+const mutation = useMutation(editTodo, {
   onSuccess: data => client.setQueryData(['todo', { id: 5 }], data),
 })
 
-mutate({
+mutation.mutate({
   id: 5,
   name: 'Do the laundry',
 })
diff --git a/docs/src/pages/quick-start.md b/docs/src/pages/quick-start.md
index a50f49648..536aaa2a5 100644
--- a/docs/src/pages/quick-start.md
+++ b/docs/src/pages/quick-start.md
@@ -10,7 +10,14 @@ This example very briefly illustrates the 3 core concepts of React Query:
 - Query Invalidation
 
 ```js
-import { useQuery, useMutation, useQueryClient, QueryCache, QueryClient, QueryClientProvider } from 'react-query'
+import {
+  useQuery,
+  useMutation,
+  useQueryClient,
+  QueryCache,
+  QueryClient,
+  QueryClientProvider,
+} from 'react-query'
 import { getTodos, postTodo } from '../my-api'
 
 // Create a cache
@@ -25,7 +32,7 @@ function App() {
     <QueryClientProvider client={client}>
       <Todos />
     </QueryClientProvider>
-  );
+  )
 }
 
 function Todos() {
@@ -33,10 +40,10 @@ function Todos() {
   const client = useQueryClient()
 
   // Queries
-  const todosQuery = useQuery('todos', getTodos)
+  const query = useQuery('todos', getTodos)
 
   // Mutations
-  const [addTodo] = useMutation(postTodo, {
+  const mutation = useMutation(postTodo, {
     onSuccess: () => {
       // Invalidate and refetch
       client.invalidateQueries('todos')
@@ -46,18 +53,18 @@ function Todos() {
   return (
     <div>
       <ul>
-        {todosQuery.data.map(todo => (
+        {query.data.map(todo => (
           <li key={todo.id}>{todo.title}</li>
         ))}
       </ul>
 
       <button
-        onClick={() =>
-          addTodo({
+        onClick={() => {
+          mutation.mutate({
             id: Date.now(),
-            title: 'Do Laundry'
+            title: 'Do Laundry',
           })
-        }
+        }}
       >
         Add Todo
       </button>
@@ -65,7 +72,7 @@ function Todos() {
   )
 }
 
-render(<App />, document.getElementById('root'));
+render(<App />, document.getElementById('root'))
 ```
 
 These three concepts make up most of the core functionality of React Query. The next sections of the documentation will go over each of these core concepts in great detail.
diff --git a/docs/src/pages/reference/useMutation.md b/docs/src/pages/reference/useMutation.md
index d7cd95c0c..e46584830 100644
--- a/docs/src/pages/reference/useMutation.md
+++ b/docs/src/pages/reference/useMutation.md
@@ -4,23 +4,29 @@ title: useMutation
 ---
 
 ```js
-const [
+const {
+  data,
+  error,
+  isError,
+  isIdle,
+  isLoading,
+  isSuccess,
   mutate,
-  { status, isIdle, isLoading, isSuccess, isError, data, error, reset },
-] = useMutation(mutationFn, {
-  onMutate,
-  onSuccess,
+  mutateAsync,
+  reset,
+  status,
+} = useMutation(mutationFn, {
   onError,
+  onMutate,
   onSettled,
-  throwOnError,
+  onSuccess,
   useErrorBoundary,
 })
 
-const promise = mutate(variables, {
-  onSuccess,
-  onSettled,
+mutate(variables, {
   onError,
-  throwOnError,
+  onSettled,
+  onSuccess,
 })
 ```
 
@@ -50,22 +56,21 @@ const promise = mutate(variables, {
   - This function will fire when the mutation is either successfully fetched or encounters an error and be passed either the data or error
   - Fires after the `mutate`-level `onSettled` handler (if it is defined)
   - If a promise is returned, it will be awaited and resolved before proceeding
-- `throwOnError`
-  - Defaults to `false`
-  - Set this to `true` if failed mutations should re-throw errors from the mutation function to the `mutate` function.
 - `useErrorBoundary`
   - Defaults to the global query config's `useErrorBoundary` value, which is `false`
   - Set this to true if you want mutation errors to be thrown in the render phase and propagate to the nearest error boundary
 
 **Returns**
 
-- `mutate: (variables: TVariables, { onSuccess, onSettled, onError, throwOnError }) => Promise<TData>`
+- `mutate: (variables: TVariables, { onSuccess, onSettled, onError }) => void`
   - The mutation function you can call with variables to trigger the mutation and optionally override the original mutation options.
   - `variables: TVariables`
     - Optional
     - The variables object to pass to the `mutationFn`.
   - Remaining options extend the same options described above in the `useMutation` hook.
   - Lifecycle callbacks defined here will fire **after** those of the same type defined in the `useMutation`-level options.
+- `mutateAsync: (variables: TVariables, { onSuccess, onSettled, onError }) => Promise<TData>`
+  - Similar to `mutate` but returns a promise which can be awaited.
 - `status: string`
   - Will be:
     - `idle` initial status prior to the mutation function executing.
diff --git a/examples/auto-refetching/pages/index.js b/examples/auto-refetching/pages/index.js
index 64123a2b9..3f9f27336 100755
--- a/examples/auto-refetching/pages/index.js
+++ b/examples/auto-refetching/pages/index.js
@@ -32,8 +32,8 @@ function Example() {
   const { status, data, error, isFetching } = useQuery(
     'todos',
     async () => {
-      const { data } = await axios.get('/api/data')
-      return data
+      const res = await axios.get('/api/data')
+      return res.data
     },
     {
       // Refetch the data every second
@@ -41,14 +41,11 @@ function Example() {
     }
   )
 
-  const [mutateAddTodo] = useMutation(
-    value => fetch(`/api/data?add=${value}`),
-    {
-      onSuccess: () => client.invalidateQueries('todos'),
-    }
-  )
+  const addMutation = useMutation(value => fetch(`/api/data?add=${value}`), {
+    onSuccess: () => client.invalidateQueries('todos'),
+  })
 
-  const [mutateClear] = useMutation(value => fetch(`/api/data?clear=1`), {
+  const clearMutation = useMutation(() => fetch(`/api/data?clear=1`), {
     onSuccess: () => client.invalidateQueries('todos'),
   })
 
@@ -86,12 +83,13 @@ function Example() {
       </label>
       <h2>Todo List</h2>
       <form
-        onSubmit={async ev => {
-          ev.preventDefault()
-          try {
-            await mutateAddTodo(value)
-            setValue('')
-          } catch {}
+        onSubmit={event => {
+          event.preventDefault()
+          addMutation.mutate(value, {
+            onSuccess: () => {
+              setValue('')
+            },
+          })
         }}
       >
         <input
@@ -106,7 +104,13 @@ function Example() {
         ))}
       </ul>
       <div>
-        <button onClick={mutateClear}>Clear All</button>
+        <button
+          onClick={() => {
+            clearMutation.mutate()
+          }}
+        >
+          Clear All
+        </button>
       </div>
       <ReactQueryDevtools initialIsOpen />
     </div>
diff --git a/examples/focus-refetching/pages/index.js b/examples/focus-refetching/pages/index.js
index 4296e0f3d..33b4bf209 100755
--- a/examples/focus-refetching/pages/index.js
+++ b/examples/focus-refetching/pages/index.js
@@ -26,15 +26,15 @@ function Example() {
   const client = useQueryClient()
 
   const { status, data, error } = useQuery('user', async () => {
-    const { data } = await axios.get('/api/user')
-    return data
+    const res = await axios.get('/api/user')
+    return res.data
   })
 
-  const [logoutMutation] = useMutation(logout, {
+  const logoutMutation = useMutation(logout, {
     onSuccess: () => client.invalidateQueries('user'),
   })
 
-  const [loginMutation] = useMutation(login, {
+  const loginMutation = useMutation(login, {
     onSuccess: () => client.invalidateQueries('user'),
   })
 
@@ -57,7 +57,7 @@ function Example() {
           <div>
             <button
               onClick={() => {
-                logoutMutation()
+                logoutMutation.mutate()
               }}
             >
               Logout
@@ -70,7 +70,7 @@ function Example() {
           <div>
             <button
               onClick={() => {
-                loginMutation()
+                loginMutation.mutate()
               }}
             >
               Login
diff --git a/examples/optimistic-updates/pages/index.js b/examples/optimistic-updates/pages/index.js
index 3998c3454..2296e1361 100755
--- a/examples/optimistic-updates/pages/index.js
+++ b/examples/optimistic-updates/pages/index.js
@@ -26,11 +26,11 @@ function Example() {
   const client = useQueryClient()
   const [text, setText] = React.useState('')
   const { status, data, error, isFetching } = useQuery('todos', async () => {
-    const { data } = await axios.get('/api/data')
-    return data
+    const res = await axios.get('/api/data')
+    return res.data
   })
 
-  const [mutatePostTodo] = useMutation(
+  const addTodoMutation = useMutation(
     text => axios.post('/api/data', { text }),
     {
       // Optimistically update the cache value on mutate, but store
@@ -72,7 +72,7 @@ function Example() {
       <form
         onSubmit={e => {
           e.preventDefault()
-          mutatePostTodo(text)
+          addTodoMutation.mutate(text)
         }}
       >
         <input
diff --git a/examples/playground/src/index.js b/examples/playground/src/index.js
index c66e09c19..03cece630 100644
--- a/examples/playground/src/index.js
+++ b/examples/playground/src/index.js
@@ -253,7 +253,7 @@ function EditTodo({ editingIndex, setEditingIndex }) {
     }
   }, [data, editingIndex]);
 
-  const [mutate, mutationState] = useMutation(patchTodo, {
+  const saveMutation = useMutation(patchTodo, {
     onSuccess: (data) => {
       // Update `todos` and the individual todo queries when this mutation succeeds
       client.invalidateQueries("todos");
@@ -261,10 +261,12 @@ function EditTodo({ editingIndex, setEditingIndex }) {
     },
   });
 
-  const onSave = () => mutate(todo);
+  const onSave = () => {
+    saveMutation.mutate(todo);
+  };
 
   const disableEditSave =
-    status === "loading" || mutationState.status === "loading";
+    status === "loading" || saveMutation.status === "loading";
 
   return (
     <div>
@@ -313,10 +315,10 @@ function EditTodo({ editingIndex, setEditingIndex }) {
             </button>
           </div>
           <div>
-            {mutationState.status === "loading"
+            {saveMutation.status === "loading"
               ? "Saving..."
-              : mutationState.status === "error"
-              ? mutationState.error.message
+              : saveMutation.status === "error"
+              ? saveMutation.error.message
               : "Saved!"}
           </div>
           <div>
@@ -338,7 +340,7 @@ function AddTodo() {
   const client = useQueryClient();
   const [name, setName] = React.useState("");
 
-  const [mutate, { status, error }] = useMutation(postTodo, {
+  const addMutation = useMutation(postTodo, {
     onSuccess: () => {
       client.invalidateQueries("todos");
     },
@@ -349,19 +351,21 @@ function AddTodo() {
       <input
         value={name}
         onChange={(e) => setName(e.target.value)}
-        disabled={status === "loading"}
+        disabled={addMutation.status === "loading"}
       />
       <button
-        onClick={() => mutate({ name })}
-        disabled={status === "loading" || !name}
+        onClick={() => {
+          addMutation.mutate({ name });
+        }}
+        disabled={addMutation.status === "loading" || !name}
       >
         Add Todo
       </button>
       <div>
-        {status === "loading"
+        {addMutation.status === "loading"
           ? "Saving..."
-          : status === "error"
-          ? error.message
+          : addMutation.status === "error"
+          ? addMutation.error.message
           : "Saved!"}
       </div>
     </div>
diff --git a/src/core/types.ts b/src/core/types.ts
index ce24bf47e..8f085aa0f 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -223,7 +223,6 @@ export interface MutateOptions<
     variables: TVariables,
     context: TContext | undefined
   ) => Promise<void> | void
-  throwOnError?: boolean
 }
 
 export interface MutationOptions<
diff --git a/src/react/types.ts b/src/react/types.ts
index da82592b3..df47a166d 100644
--- a/src/react/types.ts
+++ b/src/react/types.ts
@@ -47,23 +47,35 @@ export type MutateFunction<
 > = (
   variables: TVariables,
   options?: MutateOptions<TData, TError, TVariables, TContext>
-) => Promise<TData | undefined>
+) => void
+
+export type MutateAsyncFunction<
+  TData = unknown,
+  TError = unknown,
+  TVariables = unknown,
+  TContext = unknown
+> = (
+  variables: TVariables,
+  options?: MutateOptions<TData, TError, TVariables, TContext>
+) => Promise<TData>
 
 export interface UseMutationOptions<TData, TError, TVariables, TContext>
   extends MutationOptions<TData, TError, TVariables, TContext> {}
 
-export type UseMutationResultPair<TData, TError, TVariables, TContext> = [
-  MutateFunction<TData, TError, TVariables, TContext>,
-  UseMutationResult<TData, TError>
-]
-
-export interface UseMutationResult<TData = unknown, TError = unknown> {
-  status: MutationStatus
+export interface UseMutationResult<
+  TData = unknown,
+  TError = unknown,
+  TVariables = unknown,
+  TContext = unknown
+> {
   data: TData | undefined
   error: TError | null
+  isError: boolean
   isIdle: boolean
   isLoading: boolean
   isSuccess: boolean
-  isError: boolean
+  mutate: MutateFunction<TData, TError, TVariables, TContext>
+  mutateAsync: MutateAsyncFunction<TData, TError, TVariables, TContext>
   reset: () => void
+  status: MutationStatus
 }
diff --git a/src/react/useMutation.ts b/src/react/useMutation.ts
index 5157d5328..b1f0e7223 100644
--- a/src/react/useMutation.ts
+++ b/src/react/useMutation.ts
@@ -1,15 +1,17 @@
 import React from 'react'
 
-import { useMountedCallback } from './utils'
-import { getStatusProps } from '../core/utils'
+import { useIsMounted } from './utils'
+import { getStatusProps, noop } from '../core/utils'
 import { getLogger } from '../core/logger'
+import { notifyManager } from '../core/notifyManager'
 import { useQueryClient } from './QueryClientProvider'
 import {
+  MutateAsyncFunction,
   MutateFunction,
   MutationFunction,
   MutationStatus,
   UseMutationOptions,
-  UseMutationResultPair,
+  UseMutationResult,
 } from './types'
 
 // TYPES
@@ -98,32 +100,42 @@ export function useMutation<
 >(
   mutationFn: MutationFunction<TData, TVariables>,
   options: UseMutationOptions<TData, TError, TVariables, TContext> = {}
-): UseMutationResultPair<TData, TError, TVariables, TContext> {
-  const [state, unsafeDispatch] = React.useReducer(
+): UseMutationResult<TData, TError, TVariables, TContext> {
+  const isMounted = useIsMounted()
+  const [state, dispatch] = React.useReducer(
     reducer as Reducer<State<TData, TError>, Action<TData, TError>>,
     null,
     getDefaultState
   )
-  const dispatch = useMountedCallback(unsafeDispatch)
+
+  const safeDispatch = React.useCallback<typeof dispatch>(
+    value => {
+      notifyManager.schedule(() => {
+        if (isMounted()) {
+          dispatch(value)
+        }
+      })
+    },
+    [dispatch, isMounted]
+  )
 
   const client = useQueryClient()
   const defaultedOptions = client.defaultMutationOptions(options)
-  const latestMutationRef = React.useRef(0)
+  const latestMutationIdRef = React.useRef(0)
   const latestMutationFnRef = React.useRef(mutationFn)
   latestMutationFnRef.current = mutationFn
   const latestOptionsRef = React.useRef(defaultedOptions)
   latestOptionsRef.current = defaultedOptions
 
-  const mutate = React.useCallback<
-    MutateFunction<TData, TError, TVariables, TContext>
+  const mutateAsync = React.useCallback<
+    MutateAsyncFunction<TData, TError, TVariables, TContext>
   >(
-    async (variables, mutateOptions = {}): Promise<TData | undefined> => {
-      dispatch({ type: 'loading' })
+    async (variables, mutateOptions = {}): Promise<TData> => {
+      safeDispatch({ type: 'loading' })
 
-      const mutationId = ++latestMutationRef.current
+      const mutationId = ++latestMutationIdRef.current
       const latestOptions = latestOptionsRef.current
       const latestMutationFn = latestMutationFnRef.current
-      const isLatest = () => latestMutationRef.current === mutationId
       let context: TContext | undefined
 
       try {
@@ -136,8 +148,8 @@ export function useMutation<
         await mutateOptions.onSettled?.(data, null, variables, context)
 
         // Dispatch after handlers as the mutation could still fail
-        if (isLatest()) {
-          dispatch({ type: 'success', data })
+        if (latestMutationIdRef.current === mutationId) {
+          safeDispatch({ type: 'success', data })
         }
 
         return data
@@ -148,18 +160,29 @@ export function useMutation<
         await latestOptions.onSettled?.(undefined, error, variables, context)
         await mutateOptions.onSettled?.(undefined, error, variables, context)
 
-        if (isLatest()) {
-          dispatch({ type: 'error', error })
+        if (latestMutationIdRef.current === mutationId) {
+          safeDispatch({ type: 'error', error })
         }
 
-        if (mutateOptions.throwOnError || latestOptions.throwOnError) {
-          throw error
-        }
+        throw error
       }
     },
-    [dispatch]
+    [safeDispatch]
+  )
+
+  const mutate = React.useCallback<
+    MutateFunction<TData, TError, TVariables, TContext>
+  >(
+    (variables, mutateOptions) => {
+      mutateAsync(variables, mutateOptions).catch(noop)
+    },
+    [mutateAsync]
   )
 
+  const reset = React.useCallback(() => {
+    safeDispatch({ type: 'reset' })
+  }, [safeDispatch])
+
   React.useEffect(() => {
     const latestOptions = latestOptionsRef.current
     if (
@@ -170,13 +193,5 @@ export function useMutation<
     }
   }, [state.error])
 
-  const reset = React.useCallback(() => {
-    dispatch({ type: 'reset' })
-  }, [dispatch])
-
-  return React.useMemo(() => [mutate, { ...state, reset }], [
-    mutate,
-    state,
-    reset,
-  ])
+  return { ...state, mutate, mutateAsync, reset }
 }
diff --git a/src/react/utils.ts b/src/react/utils.ts
index bce359143..1fe0faa38 100644
--- a/src/react/utils.ts
+++ b/src/react/utils.ts
@@ -16,15 +16,3 @@ export function useIsMounted(): () => boolean {
 
   return isMounted
 }
-
-export function useMountedCallback<T extends Function>(callback: T): T {
-  const isMounted = useIsMounted()
-  return (React.useCallback(
-    (...args: any[]) => {
-      if (isMounted()) {
-        return callback(...args)
-      }
-    },
-    [callback, isMounted]
-  ) as any) as T
-}


Test patch:
diff --git a/src/react/tests/useInfiniteQuery.test.tsx b/src/react/tests/useInfiniteQuery.test.tsx
index d92af6695..102726d9d 100644
--- a/src/react/tests/useInfiniteQuery.test.tsx
+++ b/src/react/tests/useInfiniteQuery.test.tsx
@@ -207,8 +207,9 @@ describe('useInfiniteQuery', () => {
 
     renderWithClient(client, <Page />)
 
-    await waitFor(() => expect(states.length).toBe(7))
+    await sleep(300)
 
+    expect(states.length).toBe(7)
     expect(states[0]).toMatchObject({
       data: undefined,
       isFetching: true,
@@ -275,8 +276,9 @@ describe('useInfiniteQuery', () => {
 
     renderWithClient(client, <Page />)
 
-    await waitFor(() => expect(states.length).toBe(2))
+    await sleep(10)
 
+    expect(states.length).toBe(2)
     expect(states[0]).toMatchObject({
       data: undefined,
       isSuccess: false,
@@ -319,8 +321,9 @@ describe('useInfiniteQuery', () => {
 
     renderWithClient(client, <Page />)
 
-    await waitFor(() => expect(states.length).toBe(4))
+    await sleep(100)
 
+    expect(states.length).toBe(4)
     expect(states[0]).toMatchObject({
       canFetchMore: undefined,
       data: undefined,
@@ -386,8 +389,9 @@ describe('useInfiniteQuery', () => {
 
     renderWithClient(client, <Page />)
 
-    await waitFor(() => expect(states.length).toBe(5))
+    await sleep(300)
 
+    expect(states.length).toBe(5)
     expect(states[0]).toMatchObject({
       canFetchMore: undefined,
       data: undefined,
@@ -507,8 +511,9 @@ describe('useInfiniteQuery', () => {
 
     renderWithClient(client, <Page />)
 
-    await waitFor(() => expect(states.length).toBe(4))
+    await sleep(100)
 
+    expect(states.length).toBe(4)
     expect(states[0]).toMatchObject({
       canFetchMore: undefined,
       data: undefined,
@@ -577,8 +582,9 @@ describe('useInfiniteQuery', () => {
 
     renderWithClient(client, <Page />)
 
-    await waitFor(() => expect(states.length).toBe(6))
+    await sleep(100)
 
+    expect(states.length).toBe(6)
     expect(states[0]).toMatchObject({
       canFetchMore: undefined,
       data: undefined,
diff --git a/src/react/tests/useMutation.test.tsx b/src/react/tests/useMutation.test.tsx
index 43e1ccced..858d3d2c3 100644
--- a/src/react/tests/useMutation.test.tsx
+++ b/src/react/tests/useMutation.test.tsx
@@ -2,7 +2,12 @@ import { fireEvent, waitFor } from '@testing-library/react'
 import React from 'react'
 
 import { useMutation, QueryClient, QueryCache } from '../..'
-import { mockConsoleError, renderWithClient } from './utils'
+import {
+  mockConsoleError,
+  renderWithClient,
+  setActTimeout,
+  sleep,
+} from './utils'
 
 describe('useMutation', () => {
   const cache = new QueryCache()
@@ -10,7 +15,7 @@ describe('useMutation', () => {
 
   it('should be able to reset `data`', async () => {
     function Page() {
-      const [mutate, { data = '', reset }] = useMutation(() =>
+      const { mutate, data = '', reset } = useMutation(() =>
         Promise.resolve('mutation')
       )
 
@@ -44,23 +49,16 @@ describe('useMutation', () => {
     const consoleMock = mockConsoleError()
 
     function Page() {
-      const [mutate, mutationResult] = useMutation<string, Error>(
-        () => {
-          const error = new Error('Expected mock error. All is well!')
-          error.stack = ''
-          return Promise.reject(error)
-        },
-        {
-          throwOnError: false,
-        }
-      )
+      const { mutate, error, reset } = useMutation<string, Error>(() => {
+        const err = new Error('Expected mock error. All is well!')
+        err.stack = ''
+        return Promise.reject(err)
+      })
 
       return (
         <div>
-          {mutationResult.error && (
-            <h1 data-testid="error">{mutationResult.error.message}</h1>
-          )}
-          <button onClick={() => mutationResult.reset()}>reset</button>
+          {error && <h1 data-testid="error">{error.message}</h1>}
+          <button onClick={() => reset()}>reset</button>
           <button onClick={() => mutate()}>mutate</button>
         </div>
       )
@@ -83,7 +81,7 @@ describe('useMutation', () => {
 
     fireEvent.click(getByText('reset'))
 
-    expect(queryByTestId('error')).toBeNull()
+    await waitFor(() => expect(queryByTestId('error')).toBeNull())
 
     consoleMock.mockRestore()
   })
@@ -94,7 +92,7 @@ describe('useMutation', () => {
     const onSettledMock = jest.fn()
 
     function Page() {
-      const [mutate] = useMutation(
+      const { mutate } = useMutation(
         async (vars: { count: number }) => Promise.resolve(vars.count),
         {
           onSuccess: data => {
@@ -145,7 +143,7 @@ describe('useMutation', () => {
     let count = 0
 
     function Page() {
-      const [mutate] = useMutation(
+      const { mutate } = useMutation(
         (vars: { count: number }) => {
           const error = new Error(
             `Expected mock error. All is well! ${vars.count}`
@@ -160,7 +158,6 @@ describe('useMutation', () => {
           onSettled: (_data, error) => {
             onSettledMock(error?.message)
           },
-          throwOnError: false,
         }
       )
 
@@ -208,4 +205,102 @@ describe('useMutation', () => {
 
     consoleMock.mockRestore()
   })
+
+  it('should process all success callbacks in the correct order when using mutateAsync', async () => {
+    const callbacks: string[] = []
+
+    function Page() {
+      const { mutateAsync } = useMutation(async (text: string) => text, {
+        onSuccess: async () => {
+          callbacks.push('useMutation.onSuccess')
+        },
+        onSettled: async () => {
+          callbacks.push('useMutation.onSettled')
+        },
+      })
+
+      React.useEffect(() => {
+        setActTimeout(async () => {
+          try {
+            const result = await mutateAsync('todo', {
+              onSuccess: async () => {
+                callbacks.push('mutateAsync.onSuccess')
+              },
+              onSettled: async () => {
+                callbacks.push('mutateAsync.onSettled')
+              },
+            })
+            callbacks.push(`mutateAsync.result:${result}`)
+          } catch {}
+        }, 10)
+      }, [mutateAsync])
+
+      return null
+    }
+
+    renderWithClient(client, <Page />)
+
+    await sleep(100)
+
+    expect(callbacks).toEqual([
+      'useMutation.onSuccess',
+      'mutateAsync.onSuccess',
+      'useMutation.onSettled',
+      'mutateAsync.onSettled',
+      'mutateAsync.result:todo',
+    ])
+  })
+
+  it('should process all error callbacks in the correct order when using mutateAsync', async () => {
+    const consoleMock = mockConsoleError()
+
+    const callbacks: string[] = []
+
+    function Page() {
+      const { mutateAsync } = useMutation(
+        async (_text: string) => Promise.reject('oops'),
+        {
+          onError: async () => {
+            callbacks.push('useMutation.onError')
+          },
+          onSettled: async () => {
+            callbacks.push('useMutation.onSettled')
+          },
+        }
+      )
+
+      React.useEffect(() => {
+        setActTimeout(async () => {
+          try {
+            await mutateAsync('todo', {
+              onError: async () => {
+                callbacks.push('mutateAsync.onError')
+              },
+              onSettled: async () => {
+                callbacks.push('mutateAsync.onSettled')
+              },
+            })
+          } catch (error) {
+            callbacks.push(`mutateAsync.error:${error}`)
+          }
+        }, 10)
+      }, [mutateAsync])
+
+      return null
+    }
+
+    renderWithClient(client, <Page />)
+
+    await sleep(100)
+
+    expect(callbacks).toEqual([
+      'useMutation.onError',
+      'mutateAsync.onError',
+      'useMutation.onSettled',
+      'mutateAsync.onSettled',
+      'mutateAsync.error:oops',
+    ])
+
+    consoleMock.mockRestore()
+  })
 })
diff --git a/src/react/tests/utils.tsx b/src/react/tests/utils.tsx
index bbcc65670..cfccc2f9c 100644
--- a/src/react/tests/utils.tsx
+++ b/src/react/tests/utils.tsx
@@ -41,7 +41,9 @@ export function sleep(timeout: number): Promise<void> {
 
 export function setActTimeout(fn: () => void, ms?: number) {
   setTimeout(() => {
-    act(fn)
+    act(() => {
+      fn()
+    })
   }, ms)
 }