Implement Optimistic UI with the React useOptimistic hook in Next.js

Jon Meyers
InstructorJon Meyers
Share this video with your friends

Social Share Links

Send Tweet
Published a year ago
Updated a year ago

"Optimistic UI" is a pattern for updating the page immediately with the data you have available, rather than waiting for the database query to resolve. In this lesson, we use React's useOptimistic hook to create an array of tweets that we can instantly update with our new state, when the user clicks the like button.

Code Snippets

Call useOptimistic hook

const [optimisticState, addOptimisticState] = useOptimistic(
  initialState,
  (currentState, newState) => {
    // merge state
    // return new state
  }
);

Add optimistic tweet

addOptimisticTweet({
  ...tweet,
  likes: tweet.likes - 1,
  user_has_liked_tweet: !tweet.user_has_liked_tweet,
});

Resources

Instructor: When a user clicks the Like button, there's a little bit of a delay before this value is updated. This is even more noticeable if we go to the Network tab, then throttle down to Fast 3G and then click one of our buttons. You'll see there is a noticeable lag before that value changes to a 1.

Since we already know the current number of likes and whether that number should go up or down, we can update the UI optimistically rather than waiting for that value to change in supabase. Let's start by moving the rendering logic for our tweets to a new component which we want to create in the app directory.

We'll call it tweets.tsx, and we'll export our default function for Tweets. This is going to take a tweets prop. We can declare the type for tweets, it's going to be a TweetWithAuthor [] . Then in our component, we want to return our rendering logic. We need to import our Likes component from ./likes. We know tweets is going to be an array, so we can get rid of our optional chaining.

Now, up in page.tsx, we can render our Tweets component which comes in from ./tweets. We then need to pass it a tweets prop which is going to be the tweets that we got back from supabase. Now, if we go back to the browser, we should see nothing has changed.

To implement our optimistic UI, we're going to import the experimental_useOptimistic hook from react. Then, so that we don't need to change much of our code, when this becomes stable, we can alias this to useOptimistic.

Now, since we're using client-side hooks, this component needs to be a client component. Let's add the use client directive as the first line of our component. Then in our component, when we call the useOptimistic hook, we get back an array with the first value being our optimistic collection. In this case, that's going to be optimisticTweets. We then get back a function to add an OptimisticTweet.

We need to provide it the initial value. In this case, that's going to be our tweets. Then we declare a callback function which gets passed our current list of OptimisticTweets, and then the newTweet that contains our changes. Then in the body of this function, we need to specify how to merge these two values.

In our case, we want this newTweet to replace the old version of this Tweet in the array. Let's start by creating a new variable for our newOptimisticTweets. We're going to use the spread operator to take a copy of our currentOptimisticTweets. Then we want to find the index of the old version of this Tweet.

We can create a new variable for our index and set that to newOptimisticTweets.findIndex, which is going to run over each tweet. Then we want the index of the tweet where the id matches our newTweet's id. Now that we have the index of the old tweet, we can say, newOptimisticTweets at the index of the old tweet. We want to replace it with our newTweet.

Then we need to return a new value for our OptimisticTweets. We can return our new array of OptimisticTweets. TypeScript is very unhappy, so let's correctly type our useOptimistic hook to say, the type of our collection is going to be a TweetWithAuthor [] , and our new tweet is going to be a TweetWithAuthor.

Now, this may look a little intimidating. To recap, we passed it our initial value for our tweets, we then declared a function to say how we want to merge our newTweet into our list of Tweets, then we get back this optimisticTweets value, and then a function we can call any time we want to change the state of a Tweet.

Let's use our optimisticTweets array for a collection we're iterating over to display on the page. If we go back to the browser, this should look exactly the same. When we click this button, that's when we want to change the number of likes.

If we have a look at our Likes component, that will be in this handleLikes function, just before we make a call to supabase to either remove a Like or insert a new one. Let's pass down a new prop for addOptimisticTweet, which will be set to our addOptimisticTweet function. Then our Likes component is going to receive an addOptimisticTweet function.

The type for this one is going to be a function that receives our newTweet, which is of type TweetWithAuthor. It doesn't return a value, so we can specify void. Then before we make our call to supabase, we want to call our addOptimisticTweet function and pass it the new state of our tweet. We can spread in our current tweet, and then overwrite the value for likes.

In this case, we're decrementing the value. That'll be tweet.likes-1. We also need to change the state for whether our user_has _liked_tweet. We can negate the current value for our tweet.user_has _liked_tweet. Now, if we go over to the browser, we can see that when we're unliking a tweet, it's instantaneous, but when we're liking a tweet, we have to wait for that value to update in the database.

Let's make our likes optimistic as well. We can copy this block, and then before we insert a new like, we first want to increment that number of likes. Back over in the browser, we can see that liking and disliking are now instantaneous.