Arpith Siromoney 💬

Optimism in UI: Can it go too far?

Previously, we had looked at using optimistic UI to improve the performance of deleting, creating and editing posts in my app. The question was what should be done when a delete (for example) fails — we don’t want a difference between what you see on the app and what’s published online.

My first thought was to just keep retrying the request until it succeeds. However, retrying the creation of a post shouldn’t result in duplicate posts. To fix this, I’ve changed the API (not really, I’ve postponed this for now) to accept post metadata from the app, which will basically mean the API overwrites the stored post with the exact same data (for now, we can then avoid this extra write by checking the contents of the post against the stored data before doing a rewrite).

In order to keep retrying the requests until they succeed, my flux store for posts is also going to keep track of the requests made so far, and retry failed requests every time a new request is added to this list.

The code to delete posts used to look like:

function deletePost(post) {
  var i = _postURLs.indexOf(post.url);
  _postURLs.splice(i, 1);
  delete _posts[post.url];
  PostStore.emitChange();
  var username = SettingStore.getUsername();
  var token = SettingStore.getToken();
  var body = JSON.stringify({token: token});
  var key = post.key;
  if (!key) key = post.created + post.id;
  var url = APIURL + '/' + username + '/' + key;
  var params = {method: ‘DELETE’, body: body, headers: HEADERS};
  fetch(url, params).then(updateAsyncStore);
}

With the new list of requests, this now looks like:

function retryFailedRequests() {
  var promiseArr = _requests.map((req) => {
    if (req.stat !== 'succeeded') {
      return fetch(req.url, req.params)
        .then(res => res.json())
        .then(req.callback).then(() => {
          req.stat = 'succeeded';
          return req;
        }).catch(() => {
          req.stat = 'failed';
          return req;
        });
    } else {
      return req;
    }
  });
  Promise.all(promiseArr).then((res) => {
    _requests = res;
  });
}
    
function addRequest(url, params, callback) {
  _requests.push({url: url, params: params, callback: callback});
  retryRequests();
}

function deletePost(post) {
  var i = _postURLs.indexOf(post.url);
  _postURLs.splice(i, 1);
  delete _posts[post.url];
  PostStore.emitChange();
  var username = SettingStore.getUsername();
  var token = SettingStore.getToken();
  var body = JSON.stringify({token: token});
  var key = post.key;
  if (!key) key = post.created + post.id;
  var url = APIURL + '/' + username + '/' + key;
  var params = {method: ‘DELETE’, body: body, headers: HEADERS};
  addRequest(url, params, updateAsyncStore);
}

As you can see, this is already getting a bit out of hand (take a look at the file and you’ll get an idea of how complicated the methods to create/edit posts have become). Perhaps it would be simpler to treat the local state as the source of truth, and have a small method on the API that accepts a state object and updates it’s stored state appropriately. Of course, I’d love to hear your suggestions or comments.