Optimistic UI with useOptimistic
Explore how to use React 19's useOptimistic Hook to create an Optimistic UI in Next.js applications that updates instantly before server confirmation. Understand the difference between pessimistic and optimistic UI patterns, how to manage temporary state, and handle automatic rollback on server errors. Learn when and how to apply optimistic updates for better user experience in high-success, low-risk interactions.
In our last lesson, we used useActionState to create responsive forms with loading indicators. This is a clear improvement over a full page reload, but there is a trade-off: the user still has to wait for the server roundtrip before they see their new item appear. Even with a spinner, this delay can feel sluggish.
We can do better. In this lesson, we’ll use React 19’s useOptimistic Hook to update the UI instantly.
Running example: A pessimistic UI
Let’s start with a simple “pessimistic” UI. A pessimistic UI waits for the server to confirm an action was successful before updating what the user sees.
Imagine a message board where users can post messages. When a user submits the form, our app does the following:
It disables the form and shows a loading spinner (using the
isPendingstate fromuseTransition).It sends the data to the server action.
It waits for the server to process it and save it to the database.
It receives the successful response.
It rerenders the page with the new message in the list.
This is a safe and reliable pattern, but even on a fast connection, that round-trip takes time. For the user, it feels like a lag.
Here’s the code for a basic message board. First, we define a data structure as the mock database.
// app/messages/data.jsexport const messages = [{ id: 1, text: 'Hello, world!' },{ id: 2, text: 'This is the second message.' },];
Next, we create a Server Action to add a new message and revalidate the path. We’ll add a deliberate one-second delay in our Server Action to simulate real-world network latency.
// app/messages/actions.js'use server';import { revalidatePath } from 'next/cache';import { messages } from './data';export async function addMessage(messageText) {// Simulate a 1-second network delayawait new Promise((res) => setTimeout(res, 1000));const newMessage = {id: messages.length + 1,text: messageText,};messages.push(newMessage);revalidatePath('/messages');}
Explanation:
Line 3: The
'use server'directive marks this module as containing server-side functions.Line 10: We simulate network latency.
Lines 12–16: We create a new ...