DEV Community

Chan Ro
Chan Ro

Posted on • Edited on

Queue actions in optimistic updates

What is Optimistic update?

Optimistic update is a process where UI behaves as though a change was successsfully completed before backend server completes saving the data in the database. This will eventually get confirmation or error (unlikely). Overall, this provides more responsive UX.

General process of Optimistic update

In the process of updating a value in UI, the Frontend requests data change to the backend service and also changes the state on UI side without waiting for the response data from the backend.

However, sometimes we want to sync UI state with the response data from the backend since they are from source of the truth (db). Thus upon completing requests, we can re-update the state in UI with the response data if needed.

Problem 1

Updating states on UI before backend completes is okay. BUT what if user spams or interacts with UI very fast? or maybe change value of the same ID quick and frequently?

This will cause few potential problems:

  • Possible spamming call to API
  • Possible race condition issue on updating data in the database
  • Unnecessary usage of database connection pool

Good news is that this can be avoided by debouncing requests on the frontend

"Debounce" it

Debounce is a process that delays the processing of the event/action until the user has stopped typing for a given amount of time.

const debounce = (callback, delay = 1000) => {  
  let timeout;  
  return (...args) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      callback(...args);
    }, delay);
  }
}; 

const requestCall = debounce(() => console.log("request"));
requestCall();
Enter fullscreen mode Exit fullscreen mode

Since this will not execute API call until user stops interacting for a given amount of time, All potential problems from the list should resolve.

Problem 2

(This problem really depends on how much do you need to care about source of truth data from the API response)

Putting stress to the backend API and race conditioning on database part should be resolved. BUT there's one other problem with debounce that is possible data sync problem on the Frontend after getting response from the Backend API.

Let's say we have a debouncer with delay of 1 second and API request can take up to 1 second to complete. What happens if user re-updates the value right before API request completes? If we need to priortise the value on the UI side then this is no problem but what if we need to priortise the value from the API response?

Then most likely, user will be seeing the value changing optimistically (changed by user) and then changing again with the value from the API response and then change again after the most recent API request.

This is quite bad user experience seeing values keep jumping.

Hash/Stack actions

With above problems, I decided not to use debounce but creating my own action hash set which waits ongoing request before executing request in the hash.

The approach can be either hash and stack where we store latest action in the hash that needs to be executed after the current OR it can be stack LIFO (Last in first out) instead of hash and pop the latest action for next execution and sending rest of requests in the stack to server as well for logging purpose.

For example, This is my data

{
  id: 1,
  value: "Hi"
}
Enter fullscreen mode Exit fullscreen mode

Let's say, I updated with following order:

  1. Update the value to "Hello"
  2. Update the value to "World"
  3. Update the value to "!"

In this case, I do not need to care about updating value "World" on Backend side when the client-side is still waiting for the response for "Hello". And once "Hello" is completed I extract "!" into next execution.

So.. the process will be like below:
Image description
(Simplified diagram)
(Blue lines indicates the life cycle after current process finishes)

  1. Update the value to "Hello" - Request sent immediately and also pushed to a hash that handles all current processings.
  2. Update the value to "World" - Request into hash using id as key
  3. Update the value to "!" - Overrides the request(from 2.) in hash
  4. On 1. completes, we will check the pending process hash and see if there is a request, then executes the request and clear the hash.

As shown above, there are two hashers (can replace to stack w/e prefer). One for current processings and one for pending processes.
And we have pending process checker that only gets triggered after the current process execution and checks if there's pending process in the hash.

With above approach potential client side data sync issue from optimistic UI application can be resolved

Possible alternative(?)

This can also achievable using abort controller however:
Abort controller is client-side based aborting and disregard executions on the server side thus even if we have bunch of canceled requests on the client-side, server side might be still handling those requests.

Summary

Debouncing is really good solution to avoid making unnecessary requests to server and make data handling more efficients but there are few concerns that needs to rethink about when it comes to optimistic UI. And sometime, creating own process hash/stack/queue handlers on the client side might work better than debouncing.

Top comments (0)