Posted by Sten Hiedel on Jan 29, 2016

React.js app optimistic updates in Flux architecture

When somebody makes an action on a webpage, they expect instant feedback. They’re not thinking about the server responding and acknowledging a change. At MyBuilder we’re aware our customers are using mobile devices more then ever. Without optimistic updates network latency can kill our app’s perceived speed.

In theory this sound easy, but in practice it can be a real can of worms. We found that handling errors and rolling back the optimistic change can bloat the codebase, and sometimes even be difficult without reloading the affected object graph from the server.

I will go through some common challenges around optimistic updates and some possible solutions, they are no means exhaustive nor without their own problems. I’ll try to get across what we have learned while trying to implement and maintain optimistic updates as best as I can.

Setup

For this post I’ll assume basic understanding of ReactJS, Flux architecture, and ES6.

To illustrate the solutions we’ll use the following store implementation that uses Facebook’s Flux dispatcher, and some other common practices like immutable store state etc. We’ll also make extensive use of Facebook’s Immutable.js library.

The following is a bare minimum implementation of a Flux store. It differs from a commonly seen Flux store implementation, but not by much, mostly the plumbing. From outside it acts and looks as any other Flux store. The transform method is where all the state changes will be implemented, and the constructor is mostly boilerplate and plumbing but nonetheless important to understand.

And the Record object…

Optimistic action - the simple case

An optimistic action is dispatched before the server responds, and is corrected — if necessary — when we have the response from the server by dispatching another action.

When we are adding a task we can dispatch an optimistic ADD_TASK action. We need to generate a temporary clientId that will be used to remove it when server responds. If the response is successful we will dispatch RECEIVE_TASK and add the same clientId so the store can remove the temporary task object. In the event of an error, we simply remove the task by triggering an ADD_TASK_ERROR action.

The dispatcher is in module scope for actions purely for the sake of legibility.

In the store we need to change the transform method as follows:

Issues with this solution

As you can see from above, this solution for handling optimistic updates usually involves 3 different actions. This isn’t a problem in the beginning, but as the application grows — and the state changes become more involved — then it starts to get really messy. And don’t forget that an action can have an effect on multiple stores, and all of them need to roll back their changes when an error happens.

Updating the task

It gets even harder when we need to update an existing task and roll back to a previous state.

Let’s add an action to update the task…

How do you handle the update error?

When the server response throws an error, rolling back the optimistic update with another action isn’t so straightforward, because we don’t have the old state at hand. We either need to keep track of the old task state by passing it into the action, or we create a copy of it when handling the optimistic action in the store. Either way it creates more complexity that we don’t need.

A better way to handle optimistic actions

It would be great if rolling back the optimistic action was as easy as replacing it when we get the response back from the server. That would allow us to anticipate the server response, and it doesn’t really get messy while handling errors. The state changes that came from the optimistic action are rolled back in all the stores without us doing anything extra.

In order to change the action handler to handle optimistic actions we’ll need…

  • To keep track of the optimistic actions.
  • The ability to replace them with a real action.
  • To keep a copy of the whole store’s state excluding the optimistic actions.

Most of it is in the plumbing, We can update the TaskStore::constructor like so…

to make an action optimistic you need to add the optimisticActionId attribute, and to replace it add the replacesOptimisticActionId attribute to the action.

The updateTextAction would look as follows…

This looks simple enough, but we can make it even better with a helper function…

Much better, we don’t even have to know about the optimisticActionId or replacesOptimisticActionId attributes, we just get back a decorated dispatch function.

Everything after the change

Let’s change the addAction action too and then put everything together.

Be aware of drawbacks

Because the optimistic actions are stacked on top of the other actions – or known state in that matter – when an action comes that depends on the current state, and the optimistic action is still pending it might have some undesired effects. It shouldn’t be a problem in most cases because as soon as the server responds, the optimistic action is removed from the stack.

The more problematic case is when you forget to remove the optimistic action. A simple remedy would be to implement a timeout for the optimistic actions and show a warning in the console (only if in development mode). Something as simple as the following should work.

And of course look into relay combined with graphql that has a fantastic support for optimistic actions :)

Jobs at MyBuilder and Instapro

We need experienced software engineers who love their craft and want to share their hard-earned knowledge.

View vacancies
comments powered by Disqus