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.
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);
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 actionspostComment
: 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 commentcommentsFromServer
: 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 consistencyaddComment
: 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 optimisticallyConsidering 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.
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;
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:
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.
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 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:
Less suitable environments:
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.
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!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowHandle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
Design React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.