Optimistic UI updates the interface before an asynchronous operation finishes, assuming that the operation will succeed.
Instead of making a user wait for a server response before showing their comment, like, message, or form update, the interface immediately shows the expected result. If the request succeeds, the real data replaces or confirms that temporary state. If it fails, the UI can return to the previous state and show an error.
React provides the useOptimistic Hook specifically for this pattern. It was introduced in React 19 and works particularly well with React Actions and form submissions.
In this article, we will understand:
what optimistic UI actually means
why developers used to manage it manually
how
useOptimisticworkshow to use it with a form
what happens when a request succeeds or fails
common mistakes developers should avoid
when optimistic UI is—and is not—a good choice
What Is Optimistic UI?
Consider a comment form.
Without optimistic UI, the interaction might work like this:
The user writes a comment.
The user clicks Post Comment.
The browser sends the request.
The application waits for the server.
The server saves the comment.
The server responds.
The application finally displays the comment.
If the request takes one second, the interface can feel slow even though the server is working normally.
With optimistic UI, the flow changes:
The user submits the comment.
The comment appears immediately with a temporary status such as
Sending....The request runs in the background.
If the request succeeds, the confirmed server version replaces the temporary one.
If the request fails, the optimistic state disappears or is replaced with an error state.
The application is being optimistic because it temporarily assumes the operation will succeed.
This pattern is useful because perceived responsiveness often matters as much as raw network speed.
What Problem Does useOptimistic Solve?
Optimistic UI is not new.
Before useOptimistic, React developers could implement the same behavior using useState, reducers, request status flags, temporary IDs, and rollback logic.
For example, imagine a comment list stored in state:
const [comments, setComments] = useState([]);When the user submits a new comment, we could manually add it before calling the API:
async function handleSubmit(comment) {
const temporaryComment = {
id: crypto.randomUUID(),
text: comment,
pending: true,
};
setComments((current) => [
...current,
temporaryComment,
]);
try {
const savedComment = await saveComment(comment);
setComments((current) =>
current.map((item) =>
item.id === temporaryComment.id
? savedComment
: item
)
);
} catch (error) {
setComments((current) =>
current.filter(
(item) => item.id !== temporaryComment.id
)
);
}
}This approach works.
The problem is not that it is incorrect. The problem is that we are manually mixing two different kinds of state:
confirmed state that actually exists on the server
temporary optimistic state that exists only while an operation is pending
As an application becomes more complex, keeping those states synchronized can become difficult.
React introduced useOptimistic to make this temporary-state relationship explicit.
The Basic useOptimistic API
The current React API looks like this:
const [optimisticState, setOptimistic] =
useOptimistic(value, reducer);The first argument is the real or confirmed value.
The optional second argument describes how an optimistic update should transform that value.
React returns:
[
optimisticState,
setOptimistic
]optimisticState normally matches the real value.
While an Action is pending, however, React can temporarily return the optimistic version instead. The setter returned by useOptimistic is intended to be called inside an Action.
A simple example is:
const [optimisticName, setOptimisticName] =
useOptimistic(name);Inside an Action:
setOptimisticName("Jalish");React can immediately render "Jalish" while the real update is still being processed.
A Practical Comment Form Example
Let's build a more useful example.
Suppose a component receives confirmed comments:
function CommentList({ comments }) {
// ...
}We want a submitted comment to appear immediately instead of waiting for the server.
First, import useOptimistic:
import { useOptimistic } from "react";Then create an optimistic version of the comment list:
const [optimisticComments, addOptimisticComment] =
useOptimistic(
comments,
(currentComments, newComment) => [
...currentComments,
{
...newComment,
pending: true,
},
]
);The reducer receives:
current optimistic state
+
optimistic action/value
↓
next optimistic stateWe can now use it inside a form Action:
import { useOptimistic } from "react";
export default function CommentForm({
comments,
createComment,
}) {
const [
optimisticComments,
addOptimisticComment,
] = useOptimistic(
comments,
(currentComments, newComment) => [
...currentComments,
{
...newComment,
pending: true,
},
]
);
async function formAction(formData) {
const text = formData.get("comment")?.trim();
if (!text) return;
const optimisticComment = {
id: crypto.randomUUID(),
text,
};
addOptimisticComment(optimisticComment);
await createComment(text);
}
return (
<>
<form action={formAction}>
<label htmlFor="comment">
Comment
</label>
<input
id="comment"
name="comment"
type="text"
required
/>
<button type="submit">
Post Comment
</button>
</form>
<ul>
{optimisticComments.map((comment) => (
<li key={comment.id}>
{comment.text}
{comment.pending && (
<small> Sending...</small>
)}
</li>
))}
</ul>
</>
);
}React forms can receive functions through their action prop. React treats these functions as Actions, which makes them suitable for APIs such as useOptimistic.
The important line is:
addOptimisticComment(optimisticComment);The UI can now show the new comment immediately.
Meanwhile:
await createComment(text);performs the real operation.
But Where Does the Real Comment Come From?
This is one of the most important details to understand about useOptimistic.
useOptimistic does not permanently save the new state for you.
It manages temporary optimistic state.
The real state still needs to be updated through your application's normal data flow.
For example, createComment() might:
call an API
update application state after the response
invalidate a cached query
refresh server data
trigger framework-specific revalidation
Suppose the original comments are:
[
{
id: 1,
text: "First comment",
}
]The user submits:
Hello from ReactWhile the Action is pending, the optimistic list could look like:
[
{
id: 1,
text: "First comment",
},
{
id: "temporary-id",
text: "Hello from React",
pending: true,
}
]After the server succeeds, the confirmed data might become:
[
{
id: 1,
text: "First comment",
},
{
id: 42,
text: "Hello from React",
}
]The second version is the authoritative state.
The temporary ID and pending flag are no longer necessary.
This distinction prevents an important misunderstanding:
useOptimisticmanages the temporary user experience. Your server, database, cache, or application state still manages the real data.
How React Reconciles Optimistic and Real State
A useful mental model is:
Confirmed state
↓
useOptimistic
↓
Temporary optimistic state
↓
Async operation
↓
Confirmed state changes
↓
Optimistic and confirmed state convergeReact's current documentation describes optimistic state as temporary. When no relevant Action is pending, useOptimistic returns the supplied base value again. If that base value changes while an optimistic Action is pending, React can recalculate the optimistic result from the latest value.
This is important for concurrent applications because the confirmed data may change while a request is still running.
What Happens When the Request Fails?
Imagine this Action:
async function formAction(formData) {
const text = formData.get("comment");
addOptimisticComment({
id: crypto.randomUUID(),
text,
});
await createComment(text);
}The optimistic comment appears immediately.
Now suppose:
await createComment(text);throws an error.
Because the underlying confirmed comments value was never successfully changed, the optimistic state is temporary and React returns to the current base value when the Action ends.
That solves the state rollback, but it does not automatically give the user a useful explanation.
You should normally provide error feedback.
For example:
import {
useOptimistic,
useState,
} from "react";
export default function CommentForm({
comments,
createComment,
}) {
const [error, setError] = useState(null);
const [
optimisticComments,
addOptimisticComment,
] = useOptimistic(
comments,
(currentComments, newComment) => [
...currentComments,
{
...newComment,
pending: true,
},
]
);
async function formAction(formData) {
const text = formData.get("comment")?.trim();
if (!text) return;
setError(null);
addOptimisticComment({
id: crypto.randomUUID(),
text,
});
try {
await createComment(text);
} catch {
setError(
"Your comment could not be posted. Please try again."
);
}
}
return (
<>
<form action={formAction}>
<input
name="comment"
required
/>
<button type="submit">
Post Comment
</button>
</form>
{error && (
<p role="alert">
{error}
</p>
)}
<ul>
{optimisticComments.map((comment) => (
<li key={comment.id}>
{comment.text}
{comment.pending && (
<small> Sending...</small>
)}
</li>
))}
</ul>
</>
);
}Now failure has two effects:
Optimistic comment disappears
+
User receives an error messageThat produces a much clearer user experience.
useOptimistic Does Not Replace Pending State
Optimistic state and pending state solve related but different problems.
Optimistic state answers:
What should the interface show while we assume this operation will succeed?
Pending state answers:
Is this operation still running?
For form handling, React also provides useFormStatus.
For example:
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
>
{pending
? "Posting..."
: "Post Comment"}
</button>
);
}Then use it inside the form:
<form action={formAction}>
<input
name="comment"
required
/>
<SubmitButton />
</form>useFormStatus reads information about the parent form's submission status, while useOptimistic manages the temporary state you want to render during the Action.
They are complementary APIs rather than replacements for one another.
useOptimistic vs useState
A common question is:
Why not just use
useState?
You absolutely can build optimistic interfaces with useState.
The difference is primarily what the state represents.
With:
const [comments, setComments] = useState([]);you own the state and must determine when to:
add temporary data
replace temporary data
remove failed data
merge the server result
handle concurrent updates
With:
const [
optimisticComments,
addOptimisticComment,
] = useOptimistic(comments, reducer);you are telling React:
commentsis my confirmed state, but while this Action is pending, calculate and render this temporary optimistic version.
That distinction makes the intention of the code clearer.
useOptimistic therefore does not make useState obsolete.
Use useState for ordinary component state.
Use useOptimistic when you specifically need temporary state associated with an Action while waiting for confirmed state.
Simple State vs Reducer-Style useOptimistic
You do not always need the reducer form.
For a single value, this can be enough:
const [
optimisticName,
setOptimisticName,
] = useOptimistic(name);Then:
async function formAction(formData) {
const newName = formData.get("name");
setOptimisticName(newName);
await updateName(newName);
}For collections, a reducer is usually more useful:
const [
optimisticComments,
addOptimisticComment,
] = useOptimistic(
comments,
(currentComments, newComment) => [
...currentComments,
newComment,
]
);The reducer should remain pure: given the current optimistic state and the submitted value, it should return the next optimistic state without performing side effects. React's API documentation explicitly requires the reducer to be pure.
Network requests therefore belong in the Action, not inside the optimistic reducer.
Avoid this:
const [comments, addComment] =
useOptimistic(
initialComments,
async (current, comment) => {
await saveComment(comment);
return [...current, comment];
}
);Prefer:
const [comments, addComment] =
useOptimistic(
initialComments,
(current, comment) => [
...current,
comment,
]
);
async function formAction(formData) {
const comment = {
id: crypto.randomUUID(),
text: formData.get("comment"),
};
addComment(comment);
await saveComment(comment.text);
}The optimistic reducer determines UI state.
The Action performs the side effect.
Why Temporary IDs Are Useful
For optimistic list updates, the server has not yet generated the real database ID.
You therefore need a temporary key.
For browser-side applications, one option is:
crypto.randomUUID()For example:
const temporaryComment = {
id: crypto.randomUUID(),
text,
};This ID exists only to identify the optimistic item.
After the server responds, the confirmed record should normally use the ID generated by your real persistence layer.
Avoid creating optimistic elements without stable keys:
{comments.map((comment) => (
<Comment>
{comment.text}
</Comment>
))}Instead:
{comments.map((comment) => (
<Comment key={comment.id}>
{comment.text}
</Comment>
))}Stable keys allow React to correctly identify list items between renders.
Multiple Optimistic Updates
Consider a chat interface.
A user may send another message before the first request has completed.
A reducer-style optimistic update is useful here because each temporary action can be applied to the current optimistic state:
const [
optimisticMessages,
addOptimisticMessage,
] = useOptimistic(
messages,
(currentMessages, newMessage) => [
...currentMessages,
{
...newMessage,
status: "sending",
},
]
);Each submission can add another temporary message while confirmed data remains authoritative.
However, concurrency introduces application-level questions that useOptimistic cannot answer for you:
What if requests complete in a different order?
What if the same record is edited twice?
What if the server rejects one update but accepts another?
What if another user modifies the same data?
What should happen if the confirmed server value differs from what the client expected?
Those decisions depend on your application's data model and backend behavior.
useOptimistic helps represent temporary UI state; it is not a concurrency-control protocol.
When Optimistic UI Works Well
Optimistic updates are especially useful when operations are:
fast most of the time
likely to succeed
easy to undo visually
not destructive
understandable if temporarily pending
Common examples include:
Posting a comment
Display the comment immediately with:
Sending...Sending a chat message
Place the message in the conversation immediately while delivery continues.
Updating a profile field
Display the expected name or preference while saving it.
Adding an item to a list
Show the newly created item before persistence finishes.
Toggling a low-risk preference
Update the control immediately while the server stores the change.
When Optimistic UI May Be a Bad Choice
Not every operation should look successful before the server confirms it.
Be careful with actions involving:
financial transactions
stock or inventory guarantees
destructive operations that are difficult to reverse
permission changes
security-sensitive settings
workflows where server validation commonly rejects input
operations whose result depends heavily on other concurrent users
Imagine showing:
Payment completedbefore a payment processor has actually confirmed the transaction.
That would not merely be a responsive interface—it could be misleading.
In such situations, a pending UI is usually more appropriate:
Processing payment...followed by a confirmed success or failure state.
The question should therefore not be:
Can I use optimistic UI here?
A better question is:
Is temporarily showing success safe and understandable if the request later fails?
Common useOptimistic Mistakes
1. Treating optimistic state as permanent state
This is probably the biggest conceptual mistake.
addOptimisticComment(comment);does not save the comment.
Your application still needs to persist and eventually expose the confirmed value.
2. Never updating the base state
Suppose:
const [optimisticComments] =
useOptimistic(comments, reducer);but comments never changes after the request succeeds.
Once the optimistic Action finishes, React returns to the supplied base value. If the new comment is absent from that value, it can disappear.
The confirmed data must eventually reflect the successful operation.
3. Performing API requests inside the reducer
The reducer should calculate state, not perform side effects.
Bad:
async (comments, comment) => {
await fetch("/api/comments");
return [...comments, comment];
}Better:
(comments, comment) => [
...comments,
comment,
]and perform the network request in the Action.
4. Ignoring failures
Automatic rollback is not enough for good UX.
If an item disappears after an error without explanation, users may assume the application is broken.
Provide useful feedback:
<p role="alert">
Your comment could not be posted.
</p>5. Optimistically updating high-risk operations
Instant visual feedback should not come at the cost of misleading the user.
For uncertain or consequential operations, pending UI may be safer than pretending success has already happened.
useOptimistic, useFormStatus, and useActionState
React 19 introduced several APIs around Actions and form workflows, and their roles can initially seem similar.
A useful mental model is:
useOptimistic
↓
What should I temporarily show?
useFormStatus
↓
Is this form currently submitting?
useActionState
↓
What result/state did my Action return?For example, a form could use all three concepts:
User submits comment
↓
useOptimistic
Show comment immediately
↓
useFormStatus
Show "Posting..."
↓
Server Action / API request
↓
useActionState
Represent returned result or error
↓
Confirmed data updatesYou do not need all three APIs in every form.
Choose them based on the state your UI actually needs.
A Useful Mental Model
If you remember only one thing from this article, remember this separation:
Optimistic state
"What we expect to happen"
Confirmed state
"What actually happened"useOptimistic helps your UI temporarily represent the first one.
Your application still needs a reliable source for the second one.
A typical lifecycle looks like:
1. User performs an action
↓
2. Render optimistic result
↓
3. Send request
↓
┌──────┴──────┐
│ │
Success Failure
│ │
↓ ↓
Update real Keep previous
state real state
│ │
↓ ↓
Confirmed UI Rollback + errorOnce that distinction is clear, useOptimistic becomes much easier to reason about.
Final Takeaway
Optimistic UI is a user-interface strategy where you display the expected result of an asynchronous operation before the server confirms it.
React's useOptimistic Hook gives this temporary state a dedicated place in your component instead of forcing you to manually mix optimistic and confirmed data in ordinary state.
The most important principles are:
keep confirmed data as the source of truth
use optimistic state only as temporary UI
perform side effects inside Actions, not optimistic reducers
update or refresh the real data after successful operations
communicate failures clearly
use optimistic updates only where temporary success is safe and understandable
useOptimistic does not remove asynchronous complexity from your application. What it does is make one important part of that complexity—the temporary optimistic UI—more explicit and easier to manage.