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.
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
dispatcheris 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
optimisticActionIdattribute, and to replace it add the
replacesOptimisticActionIdattribute to the action.
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
replacesOptimisticActionId attributes, we just get back a decorated
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.