useOptimistic: Update the UI Instantly, Then Confirm
AI generated
</>
{ }
React 19 · useOptimistic · Optimistic UI · Server Actions
useOptimistic: update the UI instantly,
then let the server confirm it

If you wait for the server to respond before the UI moves, you are giving away perceived speed. useOptimistic solves this problem right at the core of React: the UI state jumps forward immediately, gets adopted on success, and rolls back cleanly on failure, without manual state management.

12 min read useOptimistic · addOptimistic · Server Actions · Rollback React 19 · Next.js 15 · TypeScript

1. What Optimistic UI means and why it changes UX

Optimistic UI is a design principle built on a simple observation: most user actions end up succeeding. When a user submits a comment, sets a like, or marks a task as done, that action rarely fails. Yet classic implementations wait for the server response before the UI moves at all. The result is a visual stutter, a brief freeze followed by a reaction, that feels like a system lagging behind the input it just received.

Optimistic UI flips this logic around: the UI reacts immediately, as if the action had already succeeded. The server request runs in parallel. If the server confirms, nothing visible happens, since the displayed state was already correct. If the server returns an error, the UI rolls back to the original state. For the user, the app feels like a native desktop application, without actually being offline. useOptimistic is React's native tool for implementing exactly this pattern cleanly and without manual state overhead.

2. The core principle behind useOptimistic

The useOptimistic hook takes two arguments: the current real state and an updater function. It returns a tuple: the optimistic state and an addOptimistic function. As long as no optimistic action is running, the optimistic state is identical to the real state. As soon as addOptimistic is called with a value, React invokes the updater function, passing it the current state and the given value, and renders the result immediately as the new state.

The important thing to understand: the real state stays untouched. Internally, React manages an overlay that is placed on top of the real state while an asynchronous action is running. Once the asynchronous action finishes, whether successfully or with an error, the overlay falls away and the real state returns. On success, the real state then matches the displayed state. On failure, the display automatically snaps back to the original real state. This mechanism is exactly why useOptimistic requires no manual rollback: React handles it automatically by falling back to the real state.


// Basic useOptimistic pattern: optimistic counter example
import { useOptimistic, useState, useTransition } from 'react';

function LikeButton({ postId, initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);
  const [isPending, startTransition] = useTransition();

  // useOptimistic(currentState, updaterFn)
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (currentLikes, increment) => currentLikes + increment // pure update function
  );

  const handleLike = () => {
    startTransition(async () => {
      addOptimisticLike(1); // immediate UI update, no await needed

      try {
        const updatedLikes = await likePost(postId); // actual server call
        setLikes(updatedLikes); // commit real state on success
      } catch {
        // no manual rollback needed, optimistic overlay falls away automatically
      }
    });
  };

  return (
    <button onClick={handleLike} disabled={isPending}>
      {optimisticLikes} Likes {isPending && '...'}
    </button>
  );
}

3. API and syntax in detail

The useOptimistic signature is deliberately minimal. The first parameter is the real state value, typically a value from a useState call or from a Server Component prop. The second parameter is a pure updater function that takes the current optimistic state and an arbitrary payload and returns the new optimistic state. This function must be pure: no side effects, no API calls, no mutation. It runs synchronously and immediately produces the new state snapshot.

The returned addOptimistic function expects exactly one parameter: the payload that gets passed to the updater function. This payload can be any value, a primitive, an object, or a partial update. useOptimistic must be called inside an asynchronous transition or a Server Action handler so React can correctly manage the lifetime of the overlay. The overlay stays active as long as the surrounding transition is running, that is the bridge between UI feedback and the actual completion of the async work.

4. Integration with React Server Actions

React Server Actions and useOptimistic are built for the same paradigm: the user interacts, the UI reacts instantly, the server processes the request asynchronously. Server Actions are used directly as the action prop on forms and are automatically executed by React inside a transition. That means anything triggered with addOptimistic inside the action handler automatically gets the correct transition semantics.

In practice this looks like the following: the form gets a Server Action as its action prop. Before the actual server call, or directly in the client component wrapping the form, addOptimistic is called with the predicted result. The form immediately shows the optimistic state while the Server Action runs in the background. Once it completes, React updates the real state via revalidatePath or an explicit setState call, and the overlay falls away. This pattern nearly eliminates the older approach of "set a loading state, send the request, reset the loading state".


// useOptimistic with React Server Actions in Next.js 15
'use client';
import { useOptimistic, useTransition } from 'react';
import { addTodoAction } from './actions'; // Server Action

interface Todo {
  id: string;
  text: string;
  pending?: boolean; // flag for optimistic items
}

export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
  const [todos, setTodos] = useState<Todo[]>(initialTodos);
  const [isPending, startTransition] = useTransition();

  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (currentTodos: Todo[], newTodo: Todo) => [...currentTodos, newTodo]
  );

  const handleSubmit = (formData: FormData) => {
    const text = formData.get('text') as string;

    startTransition(async () => {
      // Immediate optimistic insert with temporary id
      addOptimisticTodo({ id: `temp-${Date.now()}`, text, pending: true });

      const created = await addTodoAction(text); // Server Action
      setTodos(prev => [...prev, created]); // real state update
    });
  };

  return (
    <div>
      <form action={handleSubmit}>
        <input name="text" required />
        <button type="submit" disabled={isPending}>Add</button>
      </form>
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id} style={{ opacity: todo.pending ? 0.6 : 1 }}>
            {todo.text} {todo.pending && '(saving...)'}
          </li>
        ))}
      </ul>
    </div>
  );
}

5. Error handling and automatic rollback

The most important feature of useOptimistic is automatic rollback. It is not a feature that needs to be called explicitly, it is the direct consequence of the overlay design. When the asynchronous action completes, the overlay always falls away. If the action throws an error and no new real state is set, the UI automatically returns to the state before the optimistic update. That means a simple try-catch in the handler is enough to handle the error, no rollback logic is needed.

In practice it is a good idea to give the user feedback in the failure case, since the UI moving and then snapping back without explanation can be confusing. A separate error state or a toast notification in the catch block is the usual approach. With the new useActionState hook from React 19, this error state can be coupled directly to the form action, without needing a separate useState call. The combination of useOptimistic + useActionState + Server Action is the complete pattern for optimistic forms in modern React.

6. Optimistic updates for lists and collections

The most common use of useOptimistic is managing lists: adding, deleting, or updating entries before the server response arrives. Adding is straightforward, the new entry is appended to the list with a temporary id and replaced by the real entry once the server responds. Deleting is equally trivial: the entry is filtered out of the array inside the updater function. Updating requires a map operation over the array that replaces the entry being updated with its optimistic version.

One detail deserves attention: for temporary ids used on optimistically inserted entries, the React key prop must be kept stable. If the real entry comes back from the server with a different id and the state gets overwritten, a key change causes a full DOM re-render of that list item. In most cases this is acceptable, but it can produce visual artifacts with animations or focus management. The solution is to design the entry so that the temporary id does not end up in the key prop, and instead use a stable attribute such as clientId that survives the server round trip.


// Optimistic delete and update in a list
'use client';
import { useOptimistic, useState, useTransition } from 'react';
import { deleteItemAction, updateItemAction } from './actions';

interface Item { id: string; name: string; done: boolean; }

export function ItemList({ initial }: { initial: Item[] }) {
  const [items, setItems] = useState<Item[]>(initial);
  const [, startTransition] = useTransition();

  const [optimisticItems, dispatch] = useOptimistic(
    items,
    (state: Item[], action: { type: string; id: string; patch?: Partial<Item> }) => {
      if (action.type === 'delete') return state.filter(i => i.id !== action.id);
      if (action.type === 'update') return state.map(i =>
        i.id === action.id ? { ...i, ...action.patch } : i
      );
      return state;
    }
  );

  const handleDelete = (id: string) => startTransition(async () => {
    dispatch({ type: 'delete', id });
    const updated = await deleteItemAction(id);
    setItems(updated);
  });

  const handleToggle = (id: string) => startTransition(async () => {
    const item = items.find(i => i.id === id)!;
    dispatch({ type: 'update', id, patch: { done: !item.done } });
    const updated = await updateItemAction(id, { done: !item.done });
    setItems(updated);
  });

  return (
    <ul>
      {optimisticItems.map(item => (
        <li key={item.id}>
          <input type="checkbox" checked={item.done} onChange={() => handleToggle(item.id)} />
          {item.name}
          <button onClick={() => handleDelete(item.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

7. useOptimistic vs. manual state management

Before useOptimistic, Optimistic UI was implemented manually: set a loading state, write the predicted value into a separate state, synchronize both states once the server responds, reset the predicted state on error. This approach requires several useState calls, explicit rollback logic, and careful state syncing, sources of bugs at every corner. In complex components with several simultaneous optimistic actions, the code quickly becomes hard to follow.

useOptimistic reduces this pattern to a single hook call and an updater function. Rollback is implicit. Multiple simultaneous optimistic actions are correctly stacked and resolved cleanly by React. The code now only describes what should happen, not how the state transition is mechanically implemented. That is the real gain, not brevity for its own sake, but the elimination of an entire class of state synchronization bugs.

Aspect Manual state management useOptimistic
Rollback Manual, in the catch block Automatic (overlay design)
Concurrent updates Race conditions possible React stacks correctly
Number of useState calls 3-5 (state, loading, error, optimistic...) 1 + useOptimistic
Server Action integration Manual wiring Native, via transition
Code volume High (lots of boilerplate) Minimal

8. TypeScript integration and type safety

TypeScript and useOptimistic work well together as long as the types of the real state, the optimistic state, and the payload are clearly defined. In many cases the optimistic state has the same type as the real state, then no explicit type argument is needed, TypeScript infers everything from the first parameter. If the optimistic state has a different type, for example with an extra pending flag, the type must be given explicitly: useOptimistic<OptimisticType, PayloadType>(state, updater).

A common TypeScript mistake is making the payload type too broad (say, any or unknown), which erodes type safety inside the updater function. The recommendation: define payload types as a union of action objects when several action types flow through the same hook. This mirrors the Redux-like dispatch pattern and enables exhaustive switch statements in the updater function, with TypeScript flagging any missing cases.

9. Limits and when useOptimistic is not a fit

useOptimistic is not suited to every use case. For actions with a high failure rate, for example complex validations, payment flows, or actions with external dependencies, rollback is confusing to users when it happens often. Optimistic UI works best when the success rate is close to 100 percent and rollbacks are the exception. On unreliable networks or with long server round trips, the window between the optimistic update and the rollback can grow large enough that the user has already interacted further, with a potentially inconsistent UI display as a result.

useOptimistic is also unsuited for complex aggregations where the optimistic state is hard to predict. If the server value depends on other concurrent users or on backend logic that cannot be replicated on the client, a simple loading spinner pattern is more honest to the user. useOptimistic is not a substitute for good UX design, it amplifies what already works, it does not disguise what is fundamentally slow or error prone.


// Pattern: useOptimistic + useActionState for complete form handling
'use client';
import { useOptimistic, useActionState, useTransition } from 'react';
import { submitCommentAction } from './actions';

interface Comment { id: string; text: string; author: string; pending?: boolean; }

const initialState = { error: null as string | null };

export function CommentSection({ comments: initial }: { comments: Comment[] }) {
  const [comments, setComments] = useState<Comment[]>(initial);
  const [, startTransition] = useTransition();

  const [optimisticComments, addOptimisticComment] = useOptimistic(
    comments,
    (state: Comment[], comment: Comment) => [...state, comment]
  );

  const [actionState, formAction] = useActionState(
    async (_prev: typeof initialState, formData: FormData) => {
      const text = formData.get('text') as string;
      const tempComment: Comment = {
        id: `temp-${Date.now()}`,
        text,
        author: 'You',
        pending: true,
      };

      startTransition(() => addOptimisticComment(tempComment));

      try {
        const saved = await submitCommentAction(text);
        setComments(prev => [...prev, saved]);
        return { error: null };
      } catch {
        return { error: 'Comment could not be saved.' };
      }
    },
    initialState
  );

  return (
    <section>
      {actionState.error && <p style={{ color: 'red' }}>{actionState.error}</p>}
      <ul>
        {optimisticComments.map(c => (
          <li key={c.id} style={{ opacity: c.pending ? 0.5 : 1 }}>
            <strong>{c.author}:</strong> {c.text}
          </li>
        ))}
      </ul>
      <form action={formAction}>
        <textarea name="text" required />
        <button type="submit">Comment</button>
      </form>
    </section>
  );
}

10. Summary

useOptimistic is React's built-in answer to the classic problem of UIs that feel slow: user actions feel delayed because the display waits on the server. The hook solves this with a simple overlay mechanism: the displayed state jumps immediately to the predicted value while the real action runs asynchronously. Rollback is automatic, race conditions are managed by React, and integration with Server Actions turns the entire request-response lifecycle into a declarative flow.

The most important rules for production use: always use useOptimistic inside a transition, keep the updater function pure, define payload types precisely in TypeScript, and pair rollbacks with a brief user notification. For lists, a dispatch pattern with action types in the updater function is recommended. useOptimistic is not a universal tool for every asynchronous interaction, for actions with a realistic failure rate, a classic loading state pattern remains the more honest choice.

useOptimistic, the essentials at a glance

Automatic rollback

When the async action ends, the overlay falls away automatically, no manual reset needed. Works the same for errors and success.

Transition context

useOptimistic must run inside a transition or a Server Action, only then does React correctly know the lifetime of the overlay.

Pure updater function

The updater function must not have side effects, it only computes the new optimistic state from the current state and the payload.

Scope of application

Ideal for likes, comments, todos, form submits, anywhere the success rate is close to 100 percent and rollbacks are rare.

11. FAQ: useOptimistic in React 19

1What is useOptimistic?
A React hook that immediately jumps the UI state to the predicted value while an async action is running, with automatic rollback on error.
2Do I have to implement rollback manually?
No. The overlay falls away automatically when the transition ends. On error the UI returns to the original state, no catch block needed for the rollback itself.
3Do I need Server Actions?
No. useOptimistic works with any async operation inside a transition, classic fetch calls inside startTransition work too.
4Several updates at once?
React stacks overlays and resolves them cleanly. The scheduler prevents race conditions, without any manual queuing.
5TypeScript: how to type it?
useOptimistic<StateType, PayloadType>. Define the payload as a union of action types when several operations run through the same hook.
6When is it better to skip useOptimistic?
For payments, high failure rates, or when the server result depends on other users or complex backend logic.
7Which React version is required?
React 19 (stable). It was experimental in React 18.3. With Next.js 15 and React 19 it is production ready.
8Does it work for delete operations?
Yes. The updater function immediately filters the entry out of the list. On a server error the list returns automatically, no manual re-insertion needed.
9useOptimistic vs. SWR?
SWR's mutate() with rollbackOnError is an alternative, but requires SWR as a dependency. useOptimistic is native to React and integrates more tightly into the rendering cycle.
10What is useActionState and how does it complement useOptimistic?
useActionState couples error state directly to a form action. Combined with useOptimistic, it forms the complete pattern: instant feedback plus a clean error state without a separate useState overhead.