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).
'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>
)
}// 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
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.