DEV Community

Cover image for Understanding optimistic UI and React’s useOptimistic Hook
Megan Lee for LogRocket

Posted on • Updated on • Originally published at blog.logrocket.com

Understanding optimistic UI and React’s useOptimistic Hook

Written by Kayode Adeniyi✏️

Milliseconds can make or break user experiences, as frustration from delays can drive them away from your site. One strategy that has gained traction for its ability to deliver swift, responsive interactions is the optimistic UI pattern. This pattern allows you to update a UI immediately after a user completes an action, based on the assumption that the background operations will succeed. This method increases the perception of an app’s speed and greatly improves user experience by providing instant feedback.

In this article, we will explore React’s new useOptimistic Hook and how it can allow you to implement optimistic UI updates to make your application feel faster and more responsive.

What is the React useOptimistic Hook?

The useOptimistic Hook is a powerful feature currently available in React’s Canary version. It lets you show a different state while an asynchronous action, such as a network request, is underway. This is particularly important in cases where users expect instant feedback for their actions, such as when submitting forms or commenting on social media.

By implementing the useOptimistic Hook, applications can update user interfaces immediately, providing a seamless and engaging user experience. Let's break it down with an example:

const [optimisticComments, postComment] = useOptimistic(commentsFromServer, addComment);
Enter fullscreen mode Exit fullscreen mode
  • optimisticComments: This is the optimistic state, initialized to the value of commentsFromServer. It shows the current list of comments on a post, immediately reflecting any user actions
  • postComment: This is the dispatch function that you call to apply the mutation defined in addComment. This function updates optimisticComments optimistically when a user adds a comment
  • commentsFromServer: This is the source of truth, representing the confirmed list of comments fetched from the server. If this value changes (e.g., after fetching new data from the server), optimisticComments will also update to match, ensuring consistency
  • addComment: This is the mutation logic that will be applied to optimisticComments when postComment is called. It defines how to add a new comment to the list optimistically

Considering developer efficiency, React introduced the useOptimistic Hook tailored for this UI pattern. The optimistic UI update ensures that users are not left waiting for server responses, which is crucial for maintaining a smooth user experience. When a user performs an action, useOptimistic reflects the state changes immediately before the server responds.

This hook manages the optimistic state by initially assuming that the API will respond successfully. If the API responds successfully, the assumed state is persisted. In case of an API failure, the state reverts to its original state. This balance is crucial for maintaining data integrity while offering a user experience that feels fast and responsive.

By using the useOptimistic Hook, developers can create applications that feel more immediate and engaging, keeping users actively involved and satisfied with the performance.

A simple example: Commenting on a post

Imagine a social media platform where users can see posts from others. When a user hits the Send button, the app waits for the server’s response before updating the UI. This delay can make the app feel sluggish.

Assuming you have a server set up with the necessary files, let’s proceed to implement the optimistic functionality:

import { useState } from 'react';
import { useOptimistic } from 'react';

function CommentButton({ postId }) {
  const [comments, setComments] = useOptimistic([]);
  const [newComment, setNewComment] = useState('');
  const [error, setError] = useState('');

  const handleCommentSubmit = async () => {
    setError(''); // Clear any previous errors
    const optimisticNewComment = { id: Date.now(), text: newComment, status: 'sending' };
    setComments([...comments, optimisticNewComment]);  // Optimistically update the comments list

    try {
      const response = await api.postComment(postId, newComment);
      const updatedComments = comments.map(comment =>
        comment.id === optimisticNewComment.id ? { ...comment, status: 'sent' } : comment
      );
      setComments(updatedComments); // Update comment status to 'sent'
    } catch (error) {
      const filteredComments = comments.filter(comment => comment.id !== optimisticNewComment.id);
      setComments(filteredComments);
      setError('Failed to post comment. Please try again.');
    }
  };

  return (
    <div>
      <input 
        type="text"
        value={newComment}
        onChange={(e) => setNewComment(e.target.value)}
        placeholder="Write a comment..."
      />
      <button onClick={handleCommentSubmit} disabled={!newComment.trim()}>
        Post Comment
      </button>
      {comments.map(comment => (
        <div key={comment.id}>
          {comment.text} {comment.status === 'sending' && <span>(Sending...)</span>}
        </div>
      ))}
      {error && <p style={{ color: 'red' }}>{error}</p>}
    </div>
  );
}

export default CommentButton;
Enter fullscreen mode Exit fullscreen mode

In this code snippet, we are initializing the optimistic state. The optimisticComments state variable holds the list of comments, including any optimistic updates (comments that appear immediately as the user adds them, before server confirmation). It starts as an empty array [].

postComment is a function you use to update optimisticComments optimistically. It takes a new comment and adds it to the list of existing comments. The mutation function (comments, newComment) \=> [...comments, newComment] specifies how to update the comments list when a new comment is added. It creates a new array containing all existing comments and the new comment.

In order to submit a comment optimistically, you’d first create an optimistic comment. optimisticNewComment is a new comment object with a temporary ID, the text from the input field, and a status of sending. This object represents the comment that the user just added.

Then, to achieve an optimistic update, you’d use postComment(optimisticNewComment), which updates the optimisticComments list to include the new comment immediately. This gives the user instant feedback that their comment has been added.

Then, the try block would attempt to send the new comment to the server using api.postComment(postId, newComment). If the server confirms the comment was added, the status of optimisticNewComment is updated to sent. The list of comments is updated to reflect this new status. If the server call fails, the catch block removes the optimistic comment from optimisticComments and sets an error message to inform the user that the comment could not be posted.

When we run our application and hit the Post Comment button, it looks like this: Button To Post A Comment Button To Post Comment With Additional Message To Indicate That It Is Still Sending The Message Note how it says Sending along with the comment — that’s exactly what the useOptimistic Hook does. It posts the comment while still waiting for the network request to complete.

Optimistic UI vs. Traditional methods

By implementing the optimistic UI pattern, applications can mimic immediate responsiveness by updating user interfaces as soon as the user interacts, which is especially vital in seamless interaction environments like social media.

Traditional UI updates rely on server response times, subjecting users to long wait times and making the application feel sluggish. Optimistic UI transforms this experience by updating interfaces preemptively, showing users the expected outcomes of their actions instantly — like seeing a comment appear the moment it's clicked or a comment post immediately.

This method keeps users actively engaged and enhances their overall experience by ensuring a continuous and seamless interactive flow. This strategy is critical for maintaining user satisfaction in high-engagement platforms.

Optimistic UI in reliable environments

Optimistic UI excels in scenarios where the actions are nearly always successful, such as sending a message or preference updates within an app. In these contexts, server errors are exceptions rather than the rule, and even when they occur, they are typically non-disruptive to the overall user experience. This reliability positions optimistic UI as a key strategy for modern applications aimed at enhancing user retention and satisfaction.

When to use Optimistic UI:

  • Social media platforms: Ensuring instant feedback for actions like posting comments, liking posts, and sharing content
  • Ecommerce applications: Providing immediate updates when adding items to a cart, wish list, or during the checkout process
  • Collaborative tools: Enhancing real-time collaboration by immediately showing changes (e.g., document edits, task updates)
  • Messaging apps: Allowing users to see their messages in the conversation thread instantly after sending
  • Booking systems: Updating available slots or bookings in real time to avoid conflicts and improve user satisfaction

Less suitable environments:

  • Critical financial transactions: Where accuracy and confirmation are crucial, such as bank transfers or stock trades
  • Medical applications: When dealing with sensitive health data or actions that require verified updates
  • Systems with high latency and unreliable connections: Where the likelihood of conflicts or errors due to optimistic updates is high
  • Legal document management: Where ensuring the accuracy and legality of every change is crucial

Managing errors and reversions with useOptimistic

While optimistic UI offers numerous benefits, it also presents challenges, notably in handling the unexpected — like server errors. React’s useOptimistic Hook addresses this by enabling a smooth rollback to the UI’s previous state if an action fails.

For instance, if a “comment” on a social media post doesn't go through, the UI can automatically revert, removing the comment, and a concise error message informs the user of the hiccup. This approach not only maintains the integrity and transparency of the UI but also improves user trust by demonstrating the application’s reliability, even in the face of errors. Many modern applications use this approach to improve user interactions.

Conclusion

In this article, we walked through how React’s new useOptimistic Hook can be used to make apps feel quicker and more responsive. We looked at how a "Like" button can show an immediate response using the example of a social media app.

The useOptimistic Hook makes interactions with apps seamless and quick. Even though it might mean a little extra work to handle errors, like when we need to undo a like if something goes wrong, it's a good trade-off for a much better user experience and faster app performance.

Whether it’s chat messages, likes, or another async action, consider using useOptimistic to create a delightful user experience and make sure to handle potential errors.

Happy coding!


Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.

NPM:

$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
Enter fullscreen mode Exit fullscreen mode

Script Tag:

Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
Enter fullscreen mode Exit fullscreen mode

3.(Optional) Install plugins for deeper integrations with your stack:

  • Redux middleware
  • ngrx middleware
  • Vuex plugin

Get started now

Top comments (0)