What is Optimistic UI?
Optimistic UI is a front-end development paradigm wherein, after making a mutation request to an API, the client updates the UI optimistically assuming that the request is successful.
See the example in the GIF below. It displays a counter value from the database and the increment button increments it. The counter on the left implements the traditional synchronous UI, while the one on the right implements optimistic UI.
Synchronous UI
- The
increment
request is made to the database. - Once the response is successful, the counter value on the UI is updated.
- If the response is unsuccessful, the counter value on the UI remains unchanged.
Optimistic UI
- The
increment
request is made to the database. - The counter value on the UI is updated immediately assuming the response will be successful.
- If the response is successful, the UI is updated with the data from successful response
- If the response is unsuccessful, the counter value on the UI reverts back to the previous state.
Why should you use Optimistic UI?
As you see above, in both cases, successful and unsuccessful responses are handled elegantly. The only difference is that optimistic UI seems faster irrespective of the network bottleneck. In most cases, there is no reason to wait for a successful response, because most requests are expected to be successful in production environment. You can also have a recovery mechanism for reverting back to original state if the request is unsuccessful.
Clobbering with Optimistic UI
The Problem
Clobbering is a software-engineering problem where a source of data is overwritten due to side effects. In case of optimistic UI, clobbering usually happens when the UI makes multiple mutations in quick succession and the optimistic UI for a mutation is overwritten with the response data from a different mutation.
Consider a situation where a UI element that mutates from a value 1 to a value 2 and to a value 3 in quick succession. You can see the clobbering problem in the GIF below:
The UI will go through the following states if we try to implement optimistic UI without accounting for clobbering:
Initial state
- Value in database: 1
- Value on UI: 1
Mutation to value (2) initiated
- Value in database: 1
- Value on UI (optimistic) : 2
Mutation to value (3) initiated
- Value in database: 1
- Value on UI (optimistic): 3
Mutation to value (2) successful
- Value in database: 2
- Value on UI (from successful response): 2
Mutation to value (3) successful
- Value in database: 3
- Value on UI (from successful response): 3
This means that for a person just viewing the UI, the UI goes from 1 to 2 to 3 to 2 to 3 , which is semantically just wrong. The UI should ideally be going from 1 to 2 to 3 and that's it.
This is the clobbering problem with optimistic UI.
The Solution
We can solve this problem by checking for stale data before updating the UI. To achieve this, we associate every mutation with a unique comparable identifier such as a timestamp or a number. This means, along with each mutation, you associate an identifier (say, a number) slightly greater than the identifier associated with the previous mutation. This identifier should be a part of both, your optimistic response and the mutation response. Now, whenever the UI has to be updated, we just have to check if the new data has mutation identifier greater than that of the existing data. In this way, we avoid updating the UI with stale data.
Now that we know how to account for clobbering, lets revisit the UI states when a value goes from 1 to 2 to 3:
Initial state
- Value in database: 1
- Mutation Identifier: 1252
- Value on UI: 1
Mutation to value (2) initiated
- Value in database: 1
- Mutation Identifier of optimistic data: 1253
- Mutation Identifier of existing data: 1252
- Value on UI (optimistic) : 2
Mutation to value (3) initiated
- Value in database: 1
- Mutation Identifier of optimistic data: 1254
- Mutation Identifier of existing data: 1252
- Value on UI (optimistic): 3
Mutation to value (2) successful
- Value in database: 2
- Mutation Identifier of data from successful response: 1253
- Mutation Identifier of existing data: 1254
- Value on UI (from successful response): 3
Note that the UI does not update with the data from mutation response because the mutation identifier is less than the mutation identifier in the UI data
Mutation to value (3) successful
- Value in database: 3
- Mutation Identifier of data from successful response: 1254
- Mutation Identifier of existing data: 1254
- Value on UI (from successful response): 3
As you see, the UI goes from 1 to 2 to 3 and that's it.
The idea is to check and sanitise the data before updating a data source and it can be used to solve most clobbering issues. The only requirement for implementing this solution is that the server should support atomic increments along with updates.
Error Handling
With this solution to clobbering, it is easy to handle unsuccessful requests as well. Since every UI state is associated with a mutation identifier, we can roll back to the previous mutation identifier whenever an unsuccessful response is received from the server.
Example
Let us take an example of implementing this solution in case of a todo app.
Imagine you have a todo app that reads data from a todo
table in Postgres. The todo table would traditionally look something like this:
Since we have to account for clobbering, we will add another field to this table called update_mutation_identifier
.
Now whenever a todo is updated from active to complete to active in rapid succession, the flow would look something like that of the diagram below.
I've used Postgres (and Hasura for GraphQL) for building the examples above.
Feedback, comments and questions are very welcome! Let us know in the comments below or on our discord server.
Top comments (0)