Building an Optimistic User Interface in React

May 03, 2018 0 Comments

Building an Optimistic User Interface in React

 

 

No matter what kind of app you are working on, you will need to fetch some data, display it to the user, and enable the user to interact and update it.

This interaction and updating phase is often asynchronous by nature, as the app waits for a response from the backend whether to update the data or not. The update is usually based on the success of the action triggered by the user.

An Optimistic User Interface is when a user triggers an action and the UI updates immediately, even though there may be a request pending.

When our app is built using this kind of UI, we can update the UI right away to the success case. In case of failure, the UI will revert back to the original state.

In the user’s eyes, this UI gives us a much snappier and responsive experience. When used sparingly, optimistic UI updates can give our app a more polished and more responsive field without complexity.

Twitter is one great example of Optimistic UI. Let’s take an under-the-hood look at Twitter and see how Optimistic UI is implemented here.

Let’s see how Twitter does it. Head over to Twitter and open any tweet. Also, open up your browser’s DevTools and open up the Network tab.

Use the Filter field to take a look at only the POST favorites requests. POST favorites request is triggered when the user clicks on the heart icon.

When you click the heart icon, you will see in the that a create.json request was triggered. The request was resolved very fast and we can’t exactly see what is happening after the user clicks the heart icon. So let’s slow it down by clicking on the online button, and then selecting Slow 3G network.

When you click on the heart button again, we can see that even though the request is pending, the heart was visually updated. Once the request has completed, only then does the “number of likes” increase or decrease.

If we disable the network completely (click on the “offline” button in the DevTools), we can see that the heart icon will visually update, and then revert back to its original state due to network failure.

So how do we make our UI update immediately in order to reflect user actions, instead of traditionally waiting for the backend’s success response?

In this post, I will be showing how to implement this type of User Interface in your React App by building a Twitter Like Button. I also used Bit to share this components, so it can be viewed, played with an used from any project.

Twitter Like-Button component in React

I’ll take the above example of twitter to show you how to do this. But first, let’s set a simple React App that contains a set of tweets.

First, create a new React project using create-react-app

$ create-react-app my-app
$ cd my-app

Open the project directory in a code editor. I personally like to use VS Code. Delete all the files inside the src folder except the App.js and index.js.

Let’s begin by defining our Tweets. Create a new file inside the src folder called Tweet.js. Inside, write the following code:

Before moving forward, we will to add a few more things to our app. Let’s open the index.html file from the Public folder. Inside the header tag, add the following:

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">;
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/open-iconic/1.1.1/font/css/open-iconic-bootstrap.css">;

Next we need to create some content to show. In the App.js file in the src folder, erase the pre-existing boilerplate code and write this:

Normally, when we click on the ❤️ icon, things will happen immediately. But by turning this method into an asynchronous one, we can account for failure of the request and revert the state as needed.

Let’s implement this in our app. In the above code snippet, we are setting our state equal to initialstate. Let’s first define that above our App component.

Here, we have an array containing two tweets. This array is actually an array of objects with an ID, a number representing the number of likes per tweet. The state also contains a username and tweet content. We are mapping through it and displaying it tweet.

Next we need to define the request that the app will send after the user clicks on the heart icon. In the App.js file, write a new function called likeTweetRequest as shown here:

I want my second tweet’s like request to fail. To do so, simply add this line right below the import statements:

const shouldFail = id => [2].includes(id);

Inside the Tweet /> component, call the likeTweetRequest function like this:

onClickLike={tweetId => likeTweetRequest(tweetId, true)}

Now, run the yarn start or npm start command to open the app. If you take a look at the DevTool’s console, you will see that the request function is succeeding and failing as expected.

As you can see above, even though the request is being triggered and completed, our UI still does not update accordingly.

Here, we will use the setState function to update the component state based on previous state and toggle the ‘liked’ status of the tweet. Simultaneously, we will also increment and decrement the tweet’s likes property and add/remove the tweetId from the likedTweets array.

Take a look at the <Tweet /> in the App.js file. Here, the onClickLike accepts the tweetId, and we are going to chain the success and failure case of the tweet’s like function.

In the <Tweet /> component, replace the previous onClickLike statement with this:

onClickLike={this.onClickLike}

We now need to define the onClickLike function. just above the render() function, write the following code:

So what’s happening here? We are using the this.setState to update our previous state.

The setState function will accept another function, where the first thing we need to do is determine if the target tweet is already liked. This is done by taking a look at our previous state’s likedTweets.

Now we can figure out if that includes our target tweet. Once we know that, the app can return the new updated state. For now, I am only updating the liked tweets array.

So if the target tweet is already liked, clicking on the heart icon should unlike it. This is done using the filter function. Using filter, we are going to keep only those tweets whose id is not equal to the target tweet’s id.

If the tweet was not already liked, we are going to append that tweet to the likedTweets array.

Only thing left to do now is to increment/decrement the tweet’s likes property. We update the tweet’s array based on the current tweet’s state.tweets.

We’ll use the map function to go over each tweet and check if they are already liked or not.

If the tweet was already liked, the likes property will decrement by one. If it wasn’t liked, then the likes property will increment by one.

If the tweet that the map function is currently looking at is not the target tweet, then it needs to be returned as it is.

Instead of inlining our setState function, we can write it as an updater function.

Let’s give it a descriptive name, something like setTweetLiked. This function is going to accept the tweetId and newLiked. The function is then going to return what we had in setState, but instead of looking up the current state, we will be looking at the newLiked tweet.

We take the newLiked tweet and ask if the tweet was already liked or not, and carry out the rest of the processes accordingly.

Right above the App component, write down the following code:

function setTweetLiked(tweetId, newLiked) {
return state => {
return {
tweets: state.tweets.map(
tweet =>
tweet.id = tweetId
? {...tweet, likes: tweet.likes + (!newLiked ? -1 : 1)}
: tweet
),
likedTweets: !newLiked
? state.likedTweets.filter(id => id !
tweetId)
: [...state.likedTweets, tweetId],
};
};
}

Now to plug this inside the App component, we can invoke it inside the onClickLike like this. It requires tweetId and newLiked as arguments. This is going to be the opposite of the current liked.

class App extends Component {
state = initialState;

onClickLike = tweetId => {
    console.log(Clicked like: ${tweetId});
    console.log(Update state: ${tweetId});
    const isLiked = this.state.likedTweets.includes(tweetId);
this.setState(setTweetLiked(tweetId, !isLiked));
    likeTweetRequest(tweetId, true)
.then(() => {
console.log(then: ${tweetId});
})
.catch(() => {
console.error(catch: ${tweetId});
});
};
...
}

Now when we click the heart, we can see that the number and the heart is updating appropriately.

We have yet to handle the failure case. So let’s take care of that.

Currently, we can toggle the heart and increment or decrement the number of likes. We update our state as we’ve assumed request success but have yet to handle the request failure.

In the event of a request failing, we must revert the state which we’ve already updated since we assumed success at the time of user interaction.

Immediately on clicking the icon, we’re invoking setState using our setState updater factory here, which accepts the tweetId and the newLiked status. Rather than toggling this isLiked status, we can make use of the status it was at the time it was clicked.

Take a look at the likeTweetRequest function inside the onClickLike function. Update it’s catch function with the code shown below.

likeTweetRequest(tweetId, true)
.then(() => {
console.log(then: ${tweetId});
})
.catch(() => {
console.error(catch: ${tweetId});
this.setState(setTweetLiked(tweetId, isLiked));
});

Now if we click on this failure case, we’ll see that it updates, and at the time it fails, it reverts back to the original state.

Now we can click on something, and the UI updates immediately. Later, if the request was rejected, the UI will revert back to its original state.

But there is still one issue left to take care of. If you radiply click on the heart icon twice, you will see something like this happen.

What’s happening here is that when we rapidly click on the heart icon twice, two requests are sent to the backend. Both requests will fail, and the user will get the impression that the tweet was successfully liked.

There is a simple solution to this. We can add an instance property on our App called likeRequestPending. Let’s set it to false.

likeRequestPending = false;

So immediately after clicking the heart, the app needs to check the value of likeRequestPending. If the value of this flag is true, then the app will send an empty return, which basically means the app will do nothing until the likeRequestPending becomes false.

onClickLike = tweetId => {
console.log(Clicked like: ${tweetId});

if (this.likeRequestPending) {
console.log('Request already pending! Do nothing.');
return;
}
console.log(Update state: ${tweetId});
const isLiked = this.state.likedTweets.includes(tweetId);
this.setState(setTweetLiked(tweetId, !isLiked));
this.likeRequestPending = true;
...
}

Once the request is completed (in success or failure), the value of likeRequestPending is then changed back to false like this:

likeTweetRequest(tweetId, true)
.then(() => {
console.log(then: ${tweetId});
})
.catch(() => {
console.error(catch: ${tweetId});
this.setState(setTweetLiked(tweetId, isLiked));
})
.then(() => {
this.likeRequestPending = false;
})

With this, we have solved the doubly updating problem.

You can take a look at the entire source code and the components here:


Tag cloud