Mastering Mutations in React Query
Tags: react, javascript, typescript, webdev
Published: 2021-10-10 18:57:02

We've covered a lot of ground already when it comes to the features and concepts React Query provides. Most of them are about _retrieving_ data - via the _useQuery_ hook. There is however a second, integral part to working with data: updating it.

For this use-case, React Query offers the _useMutation_ hook.

# What are mutations?

Generally speaking, mutations are functions that have a side effect. As an example, have a look at the _push_ method of Arrays: It has the side effect of _changing_ the array in place where you're pushing a value to:

```js
const myArray = [1]
myArray.push(2)

console.log(myArray) // [1, 2]
```

The _immutable_ counterpart would be _concat_, which can also add values to an array, but it will return a new Array instead of directly manipulating the Array you operate on:

```js
const myArray = [1]
const newArray = myArray.concat(2)

console.log(myArray) //  [1]
console.log(newArray) // [1, 2]
```

As the name indicates, _useMutation_ also has some sort of side effect. Since we are in the context of [managing server state](https://tkdodo.eu/blog/react-query-as-a-state-manager) with React Query, mutations describe a function that performs such a side effect _on the server_. Creating a todo in your database would be a mutation. Logging in a user is also a classic mutation, because it performs the side effect of creating a token for the user.

In some aspects, _useMutation_ very similar to _useQuery_. In others, it is quite different.

# Similarities to useQuery

_useMutation_ will track the state of a mutation, just like _useQuery_ does for queries. It'll give you _loading_, _error_ and _status_ fields to make it easy for you to display what's going on to your users.

You'll also get the same nice callbacks that _useQuery_ has: _onSuccess_, _onError_ and _ onSettled_. But that's about where the similarities end.

# Differences to useQuery

> _useQuery_ is declarative, _useMutation_ is imperative.

By that, I mean that queries mostly run automatically. You define the dependencies, but React Query takes care of running the query immediately, and then also performs smart background updates when deemed necessary. That works great for queries because we want to keep what we see on the screen _in sync_ with the actual data on the backend.

For mutations, that wouldn't work well. Imagine a new todo would be created every time you focus your browser window 🤨. So instead of running the mutation instantly, React Query gives you a function that you can invoke whenever you want to make the mutation:

```jsx
function AddComment({ id }) {
  // this doesn't really do anything yet
  const addComment = useMutation((newComment) =>
    axios.post(`/posts/${id}/comments`, newComment)
  )

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault()
        // ✅ mutation is invoked when the form is submitted
        addComment.mutate(new FormData(event.currentTarget).get('comment'))
      }}
    >
      <textarea name="comment" />
      <button type="submit">Comment</button>
    </form>
  )
}
```

Another difference is that mutations don't share state like _useQuery_ does. You can invoke the same _useQuery_ call multiple times in different components and will get the same, cached result returned to you - but this won't work for mutations.

# Tying mutations to queries

Mutations are, per design, not directly coupled to queries. A mutation that likes a blog post has no ties towards the query that fetches that blog post. For that to work, you would need some sort of underlying schema, which React Query doesn't have.

To have a mutation reflect the changes it made on our queries, React Query primarily offers two ways:

## Invalidation

This is conceptually the simplest way to get your screen up-to-date. Remember, with server state, you're only ever displaying a snapshot of data from a given point in time. React Query tries to keep that up-to-date of course, but if you're deliberately changing server state with a mutation, this is a great point in time to tell React Query that some data you have cached is now "invalid". React Query will then go and refetch that data if it's currently in use, and your screen will update automatically for you once the fetch is completed. The only thing you have to tell the library is _which_ queries you want to invalidate:

```jsx
const useAddComment = (id) => {
  const queryClient = useQueryClient()

  return useMutation(
    (newComment) => axios.post(`/posts/${id}/comments`, newComment),
    {
      onSuccess: () => {
        // ✅ refetch the comments list for our blog post
        queryClient.invalidateQueries(['posts', id, 'comments'])
      },
    }
  )
}
```

Query invalidation is pretty smart. Like all [Query Filters](https://react-query.tanstack.com/guides/filters#query-filters), it uses fuzzy matching on the query key. So if you have multiple keys for your comments list, they will all be invalidated. However, only the ones that are currently active will be refetched. The rest will be marked as stale, which will cause them to be refetched the next time they are used.

As an example, let's assume we have the option to sort our comments, and at the time the new comment was added, we have two queries with comments in our cache:

```
['posts', 5, 'comments', { sortBy: ['date', 'asc'] }
['posts', 5, 'comments', { sortBy: ['author', 'desc'] }
```

Since we're only displaying one of them on the screen, _invalidateQueries_ will refetch that one and mark the other one as stale.

## Direct updates

Sometimes, you don't want to refetch data, especially if the mutation already returns everything you need to know. If you have a mutation that updates the title of your blog post, and the backend returns the complete blog post as a response, you can update the query cache directly via _setQueryData_:

```jsx
const useUpdateTitle = (id) => {
  const queryClient = useQueryClient()

  return useMutation(
    (newTitle) => axios.patch(`/posts/${id}`, { title: newTitle }),
    {
      // 💡 response of the mutation is passed to onSuccess
      onSuccess: (newPost) => {
        // ✅ update detail view directly
        queryClient.setQueryData(['posts', id], newPost)
      },
    }
  )
}
```

Putting data into the cache directly via _setQueryData_ will act as if this data was returned from the backend, which means that all components using that query will re-render accordingly.

I'm showing some more examples of direct updates and the combination of both approaches in [#8: Effective React Query Keys](https://tkdodo.eu/blog/effective-react-query-keys#structure).

---

I personally think that most of the time, invalidation should be preferred. Of course, it depends on the use-case, but for direct updates to work reliably, you need more code on the frontend, and to some extent duplicate logic from the backend. Sorted lists are for example pretty hard to update directly, as the position of my entry could've potentially changed because of the update. Invalidating the whole list is the "safer" approach.

# Optimistic updates

Optimistic updates are one of the key selling points for using React Query mutations. The _useQuery_ cache give us data instantly when switching between queries, especially when combined with [prefetching](https://react-query.tanstack.com/guides/prefetching). Our whole UI feels very snappy because of it, so why not get the same advantage for mutations as well?

A lot of the time, we are quite certain that an update will go through. Why should the user wait for a couple of seconds until we get the okay from the backend to show the result in the UI? The idea of optimistic updates is to fake the success of a mutation even before we have sent it to the server. Once we get a successful response back, all we have to do is invalidate our view again to see the real data. In case the request fails, we're going to roll back our UI to the state from before the mutation.

This works great for small mutations where instant user feedback is actually required. There is nothing worse than having a toggle button that performs a request, and it doesn't react at all until the request has completed. Users will double or even triple click that button, and it will just feel "laggy" all over the place.

## Example

I've decided to _not_ show an additional example. The [official docs](https://react-query.tanstack.com/guides/optimistic-updates) cover that topic very well, and they also have a codesandbox example [in JavaScript](https://react-query.tanstack.com/examples/optimistic-updates) and [in TypeScript](https://react-query.tanstack.com/examples/optimistic-updates-typescript).

I further think that optimistic updates are a bit over-used. Not every mutation needs to be done optimistically. You should really be sure that it rarely fails, because the UX for a rollback is not great. Imagine a Form in a Dialog that closes when you submit it, or a redirect from a detail view to a list view after an update. If those are done prematurely, they are hard to undo.

Also, be sure that the instant feedback is really required (like in the toggle button example above). The code needed to make optimistic updates work is non-trivial, especially compared to "standard" mutations. You need to mimic what the backend is doing when you're faking the result, which can be as easy as flipping a Boolean or adding an item to an Array, but it might also get more complex really fast:

- If the todo you're adding needs an id, where do you get it from?
- If the list you're currently viewing is sorted, will you insert the new entry at the right position?
- What if another user has added something else in the meantime - will our optimistically added entry switch positions after a refetch?

All these edge cases might make the UX actually worse in some situations, where it might be enough to disable the button and show a loading animation while the mutation is in-flight. As always, choose the right tool for the right job.

# Common Gotchas

Finally, let's dive into some things that are good to know when dealing with mutations that might not be that obvious initially:

## awaited Promises

Promises returned from the mutation callbacks are awaited by React Query, and as it so happens, _invalidateQueries_ returns a Promise. If you want your mutation to stay in _loading_ state while your related queries update, you have to return the result of _invalidateQueries_ from the callback:

```js
{
  // 🎉 will wait for query invalidation to finish
  onSuccess: () => {
    return queryClient.invalidateQueries(['posts', id, 'comments'])
  }
}
{
  // 🚀 fire and forget - will not wait
  onSuccess: () => {
    queryClient.invalidateQueries(['posts', id, 'comments'])
  }
}
```

## Mutate or MutateAsync

_useMutation_ gives you two functions - _mutate_ and _mutateAsync_. What's the difference, and when should you use which one?

_mutate_ doesn't return anything, while _mutateAsync_ returns a Promise containing the result of the mutation. So you might be tempted to use _mutateAsync_ when you need access to the mutation response, but I would still argue that you should almost always use _mutate_.

You can still get access to the _data_ or the _error_ via the callbacks, and you don't have to worry about error handling: Since _mutateAsync_ gives you control over the Promise, you also have to catch errors manually, or you might get an [unhandled promise rejection](https://stackoverflow.com/questions/40500490/what-is-an-unhandled-promise-rejection).

```js
const onSubmit = () => {
  // ✅ accessing the response via onSuccess
  myMutation.mutate(someData, {
    onSuccess: (data) => history.push(data.url),
  })
}

const onSubmit = async () => {
  // 🚨 works, but is missing error handling
  const data = await myMutation.mutateAsync(someData)
  history.push(data.url)
}

const onSubmit = async () => {
  // 😕 this is okay, but look at the verbosity
  try {
    const data = await myMutation.mutateAsync(someData)
    history.push(data.url)
  } catch (error) {
    // do nothing
  }
}
```

Handling errors is not necessary with _mutate_, because React Query catches (and discards) the error for you internally. It is literally implemented with: *mutateAsync().catch(noop)*😎

The only situations where I've found _mutateAsync_ to be superior is when you really need the Promise for the sake of having a Promise. This can be necessary if you want to fire off multiple mutations concurrently and want to wait for them all to be finished, or if you have dependent mutations where you'd get into callback hell with the callbacks.

## Mutations only take one argument for variables

Since the last argument to _mutate_ is the options object, _useMutation_ can currently only take _one_ argument for variables. This is certainly a limitation, but it can be easily worked around by using an object:

```jsx
// 🚨 this is invalid syntax and will NOT work
const mutation = useMutation((title, body) => updateTodo(title, body))
mutation.mutate('hello', 'world')

// ✅ use an object for multiple variables
const mutation = useMutation(({ title, body }) => updateTodo(title, body))
mutation.mutate({ title: 'hello', body: 'world' })
```

To read more on why that is currently necessary, have a look at [this discussion](https://github.com/tannerlinsley/react-query/discussions/1226).

## Some callbacks might not fire

You can have callbacks on _useMutation_ as well as on _mutate_ itself. It is important to know that the callbacks on _useMutation_ fire before the callbacks on _mutate_. Further, the callbacks on _mutate_ might not fire _at all_ if the component unmounts before the mutation has finished.

That's why I think it's a good practice to separate concerns in your callbacks:

- Do things that are absolutely necessary and logic related (like query invalidation) in the _useMutation_ callbacks.
- Do UI related things like redirects or showing toast notifications in _mutate_ callbacks. If the user navigated away from the current screen before the mutation finished, those will purposefully not fire.

This separation is especially neat if _useMutation_ comes from a custom hook, as this will keep query related logic in the custom hook while UI related actions are still in the UI. This also makes the custom hook more reusable, because how you interact with the UI might vary on a case by case basis - but the invalidation logic will likely always be the same:

```js
const useUpdateTodo = () =>
  useMutation(updateTodo, {
    // ✅ always invalidate the todo list
    onSuccess: () => {
      queryClient.invalidateQueries(['todos', 'list'])
    },
  })

// in the component

const updateTodo = useUpdateTodo()
updateTodo.mutate(
  { title: 'newTitle' },
  // ✅ only redirect if we're still on the detail page
  // when the mutation finishes
  { onSuccess: () => history.push('/todos') }
)
```

---

That's it for today. Feel free to reach out to me on [twitter](https://twitter.com/tkdodo)
if you have any questions, or just leave a comment below ⬇️