Prevent Doubly Updating and Reverting State in React

Erik Aybar
InstructorErik Aybar
Share this video with your friends

Social Share Links

Send Tweet
Published 6 years ago
Updated 5 years ago

Optimistic UI updates come with challenges that can vary for every use case. In our scenario of “liking a tweet”, one challenge that we face is preventing doubly updating and reverting state in the event of the user clicking “like” in rapid succession. In the case of a request failure our current solution results in the false impression that the tweet was successfully liked.

One solution that fits our use case is to simply raise and lower a boolean flag to indicate whether a like tweet request is pending or not and short circuit our onClickLike method to "do nothing" if a request is already pending. Since we don’t require this information in our render method or on component state, we can simplify this further by making use of a class instance property to store this value.

Instructor: [00:00] Our heart icons are optimistically updating so that when we click them, they visually immediately update. Only in the event of a failure does it revert after the request is failed. If we look a little deeper, we'll see that we have a use case where a bug will arise if we click this rapidly. Ultimately, both requests failed when you get the visual impression here that the tweet was successfully liked.

[00:27] If we follow the timeline of events, if I click this twice in rapid succession, the first time we clicked it we transitioned to liking the tweet, so that the second time we clicked it, when we came here we said, "Oh, this is already liked."

[00:43] We again updated it to toggle that, which transitioned us to not liking it. When this request failed, we reverted state, swapping back to the current liked status, which it was already liked from the first optimistic update. We need to introduce a way to prevent multiple or conflicting invocations.

[01:06] One simplified solution we can introduce is we can add an instance property on here. We'll call this, "like request pending." This is going to begin as false. We could add this into our component state. Since we don't need it in render, we can apply this as an instance property and keep things a little bit simpler.

[01:28] We want to introduce this before we update our state. Immediately after we click this, what we want to do is we want to check our flag here that we're going to be raising and lowering. Ask if this like request pending, what we're going to do is, we're going to return and short-circuit this whole thing. We will log out here request already pending. Now, we can raise our flag here by saying, this like request pending is true.

[01:57] Once this request is completed in either the success or failure case, we're going to lower the flag. We will add an additional then handler here. We will set that flag to false. Now, if we come in here and click any of these in rapid succession, we'll see that we click and update our state immediately and fire off a request. The second time we click it, the request is already pending so that we've done nothing.

[02:23] To look at our failure case, before we had the bug where we would doubly revert our state, giving the false impression that the tweet was successfully liked. Now, we've caught and prevented that.

Andrey Bakhvalov
Andrey Bakhvalov
~ 6 years ago

Hi! As I see the flag "likeRequestPending" don't allow to make parallels optimistic updates, because we will wait until a first request will complete or failure. Am I right?

Erik Aybar
Erik Aybarinstructor
~ 6 years ago

Hi Andrey, good catch and question! You're right that as-is likeRequestPending doesn't account for multiple/parallel requests.

If that did become a requirement, the current solution could be modified to support that. One solution is to change likeRequestPending from a boolean into an array of ids such as pendingTweetRequests to track pending requests for individual tweets by id. Example:

class App extends React.Component {
  // An array of tweet ids representing pending requests
  pendingTweetRequests = [];

  onClickLike = tweetId => {
    // 1) Is a request already pending for this tweet?
    if (this.pendingTweetRequests.includes(tweetId)) {
      return;
    }

    // ...

    // 2) Add this tweet to pendingTweetRequests
    this.pendingTweetRequests = this.pendingTweetRequests.concat(tweetId);

    likeTweetRequest(tweetId, !isLiked)
      // ...
      .then(() => {
        // 3) Remove tweet from pendingTweetRequests
        this.pendingTweetRequests = this.pendingTweetRequests
          .filter(id => id !== tweetId);
      });
  };

  // ...
}
Saloni Jain
Saloni Jain
~ 5 years ago

Scenario:

  1. Tweet is not liked by default.
  2. User clicks on like, we optimistically update to liked.
  3. while the first request is in progress, user realizes they want to revert so they click again to unlike, we do nothing and return as the first request is in progress.
  4. the user's second click is lost, and the UI does not behave as they expect it to.

Is this understanding correct ? If yes, is there an easy way to not let this happen?

Markdown supported.
Become a member to join the discussionEnroll Today