Taming network with redux-requests, part 6 - Optimistic updates
Tags: webdev, redux, react, javascript
Published: 2020-07-27 22:31:42

In the [previous part of this series](https://dev.to/klis87/taming-network-with-redux-requests-part-5-graphql-4g6n) we discussed the usage with GraphQL.

In this part we will cover optimistic updates and how [redux-requests](https://redux-requests.klisiczynski.com) can make them reliable and avoid some common traps.

## What are optimistic updates?

Sometimes you don't want to wait for a mutation response to update your data. If you can predict in advance how data will be updated, you might want to update it immediately even before the server response. This can improve perceived performance of your app and is known as **optimistic update**.

## Optimistic update example

Let's write a normal mutation first:
```js
const likeBook = book => ({
  type: LIKE_BOOK,
  request: {
    url: `/book/${book.id}/like`,
    method: 'put',
  },
  meta: {
    mutations: {
      FETCH_BOOKS: (data, mutationData) => 
        data.map(v => book.id === v.id ? mutationData : v),
    },
  },
});
```

How to refactor it to optimistic update? Let's assume that books have `id` and `numberOfLikes` attributes, for instance `{ id: '1', numberOfLikes: 10 }`. You can do it like that:
```js
const likeBook = book => ({
  type: LIKE_BOOK,
  request: {
    url: `/book/${book.id}/like`,
    method: 'put',
  },
  meta: {
    mutations: {
      FETCH_BOOKS: {
        updateDataOptimistic: data => 
          data.map(v => book.id === v.id ? book : v),
      },
    },
  },
});
```

So, above we have a mutation action with optimistic update for `FETCH_BOOKS` query. `updateDataOptimistic` is called right away after `LIKE_BOOK` action is dispatched, so not on success like for normal mutations.

## Error handling

There is one problem though, what if our optimism is... too optimistic? Any request can potentially fail. With normal mutation, we can just handle error and of course we won't update data. But here we do without even knowing whether the mutation will succeed. Because of that, we need to tell the library how to revert optimistic update. We can use `revertData` for that:
```js
const likeBook = book => ({
  type: LIKE_BOOK,
  request: {
    url: `/book/${book.id}/like`,
    method: 'put',
  },
  meta: {
    mutations: {
      FETCH_BOOKS: {
        updateDataOptimistic: data =>
          data.map(v => (book.id === v.id ? book : v)),
        revertData: data =>
          data.map(v =>
            book.id === v.id ? { ...v, numberOfLikes: v.numberOfLikes - 1 } : v,
          ),
      },
    },
  },
});
```

`revertData` is called on `LIKE_BOOK_ERROR`, so you can amend the data and revert deletion in case of an error.

You might ask, why `revertData` is even needed, cannot this be figured out automatically? Indeed there are some libraries, even very famous ones, which use a different approach. They copy state before optimistic updates and revert it back for you. Let's simulate this:

1. We have `{ id: '1', numberOfLikes: 10 }` book
2. We optimistically update it to `{ id: '1', numberOfLikes: 11 }`, make AJAX request and copy previous state `{ id: '1', numberOfLikes: 10 }` just in case.
3. The request failed, we update book back to `{ id: '1', numberOfLikes: 10 }`

So far so good. But this simplified approach doesn't take race conditions and concurrent requests at all into account. Imagine another scenario:

1. We have `{ id: '1', numberOfLikes: 10 }` book
2. We optimistically update it to `{ id: '1', numberOfLikes: 11 }`, make AJAX request and copy previous state `{ id: '1', numberOfLikes: 10 }` just in case.
3. Before above mutation is finished, the user is super quick and executes this mutation once more (we assume one person can like a book many times)
4. So, we optimistically update book to `{ id: '1', numberOfLikes: 12 }`, make another AJAX request and copy previous state `{ id: '1', numberOfLikes: 11 }` just in case.
5. Now, many combinations will be problematic, but imagine the simplest one, both requests will fail, in the order they were sent.
6. We receive error for the first mutation, so book is reverted to `{ id: '1', numberOfLikes: 10 }` - this is wrong, it should be `11`, update of 2nd mutation is gone
7. We receive error for the second mutation, so book is reverted to `{ id: '1', numberOfLikes: 11 }` - this is again wrong, it should be `10`, the initial value. 

Imagine other scenarios, like combined successes with errors, responses received in different order than requests, many things could go wrong with the automated solution.

## Updating data after server response

Even when using optimistic update, at the very same time you can still use `updateData` to further update data on success response. This might be useful if you cannot predict data update fully. For example you might want to do optimistic update to add an item with random id and amend it to a proper
id value once mutation response is delivered.

## What's next?

In the [next part](https://dev.to/klis87/taming-network-with-redux-requests-part-7-caching-2i3c) of the series we will cover caching.