Taming network with redux-requests, part 6 - Optimistic updates

klis87

Konrad LisiczyƄski

Posted on July 27, 2020

Taming network with redux-requests, part 6 - Optimistic updates

In the previous part of this series we discussed the usage with GraphQL.

In this part we will cover optimistic updates and how redux-requests 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:

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:

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:

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 of the series we will cover caching.

💖 đŸ’Ș 🙅 đŸš©
klis87
Konrad LisiczyƄski

Posted on July 27, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related