NextjsOptimistic Updates with useOptimistic

Optimistic Updates with useOptimistic

A Server Action round trip takes time — a network request, server processing, a response. Waiting for all of that before updating the UI can make an app feel sluggish for simple interactions like liking a post or checking off a to-do item. useOptimistic lets you update what the user sees immediately, then reconcile with whatever the server actually returns once the action completes.

The useOptimistic hook

useOptimistic takes the current state and an update function, and returns an optimistic version of that state plus a function to set it. Call the setter right when the user takes an action — React shows the optimistic value immediately, then automatically reverts to the real state once the underlying async work finishes (or throws).

TSX
'use client'

import { useOptimistic, useState } from 'react'
import { toggleLike } from './actions'

interface Post {
  id: string
  likedByMe: boolean
  likeCount: number
}

export function LikeButton({ post }: { post: Post }) {
  const [optimisticPost, setOptimisticPost] = useOptimistic(
    post,
    (currentPost, likedByMe: boolean) => ({
      ...currentPost,
      likedByMe,
      likeCount: currentPost.likeCount + (likedByMe ? 1 : -1),
    }),
  )

  async function handleClick() {
    const nextLiked = !optimisticPost.likedByMe

    // Update the UI immediately, before the server has responded
    setOptimisticPost(nextLiked)

    // Then perform the real mutation
    await toggleLike(post.id, nextLiked)
  }

  return (
    <button onClick={handleClick}>
      {optimisticPost.likedByMe ? '♥' : '♡'} {optimisticPost.likeCount}
    </button>
  )
}

TSX
// app/[locale]/feed/actions.ts
'use server'

import { revalidatePath } from 'next/cache'

export async function toggleLike(postId: string, liked: boolean) {
  await saveLikeToDatabase(postId, liked)
  revalidatePath('/feed')
}

The button flips its heart icon and count the instant it's clicked. When toggleLike resolves and the page revalidates with the real data, React reconciles optimisticPost back to the true server-confirmed post — in the common case, users never notice the transition because the optimistic value already matched.

Handling failure and reconciliation
Warning
useOptimistic assumes the mutation will usually succeed. If toggleLike throws or the server rejects the change, the optimistic state reverts to whatever the last real state was — but the user already saw the "successful" version for a moment. Always wrap the action call in a try/catch and surface an error (a toast, an inline message) so a failed mutation doesn't silently disappear without explanation.

TSX
async function handleClick() {
  const nextLiked = !optimisticPost.likedByMe
  setOptimisticPost(nextLiked)

  try {
    await toggleLike(post.id, nextLiked)
  } catch (err) {
    // The optimistic state will revert automatically once this
    // component re-renders with the real post prop, but you should
    // still tell the user something went wrong.
    showErrorToast('Could not update like — please try again.')
  }
}
When to reach for useOptimistic
  • Low-stakes, easily reversible actions: likes, follows, checkbox toggles, star ratings.

  • Actions where the eventual server-confirmed state is very likely to match what the user expects to see.

  • Interactions where waiting for a full round trip would make the UI feel noticeably unresponsive.

Note
For higher-stakes mutations — payments, deleting data, anything where a wrong optimistic guess would be confusing or costly — it is usually better to show a real pending state (see useFormStatus / useActionState) and wait for actual confirmation rather than assume success.
Tip
useOptimistic pairs naturally with Server Actions and revalidatePath/revalidateTag: the optimistic update gives instant feedback, and the subsequent revalidation guarantees the UI eventually matches the source of truth, even if the optimistic guess was slightly off.