Skip to content

Commit

Permalink
feat: split mutate into mutate and mutateAsync (#1130)
Browse files Browse the repository at this point in the history
* feat: split mutate into mutate and mutateAsync

* docs: Update docs/src/pages/guides/mutations.md

Co-authored-by: Tanner Linsley <[email protected]>
  • Loading branch information
boschni and tannerlinsley authored Oct 4, 2020
1 parent fee1376 commit 8da45d1
Show file tree
Hide file tree
Showing 16 changed files with 342 additions and 185 deletions.
4 changes: 2 additions & 2 deletions docs/src/pages/guides/invalidations-from-mutations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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')
Expand Down
98 changes: 59 additions & 39 deletions docs/src/pages/guides/mutations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>
</>
Expand Down Expand Up @@ -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>
Expand All @@ -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}
Expand All @@ -111,73 +116,88 @@ 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!")
},
onSettled: async () => {
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:

- `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)
}
```
4 changes: 2 additions & 2 deletions docs/src/pages/guides/updates-from-mutation-responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -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',
})
Expand Down
27 changes: 17 additions & 10 deletions docs/src/pages/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,18 +32,18 @@ function App() {
<QueryClientProvider client={client}>
<Todos />
</QueryClientProvider>
);
)
}

function Todos() {
// Access the client
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')
Expand All @@ -46,26 +53,26 @@ 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>
</div>
)
}

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.
33 changes: 19 additions & 14 deletions docs/src/pages/reference/useMutation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
```

Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 8da45d1

Please sign in to comment.