// LOADING
// LOADING
// LOADING_ARTICLE
Perceived performance matters as much as actual performance. An action that takes 200ms but shows no feedback until it completes feels sluggish. An action that takes 500ms but updates the UI immediately feels instant. The pattern of optimistically updating state before the server confirms is well-established, but it has historically required a lot of manual wiring. React 19's useOptimistic hook makes it first-class.
useOptimistic takes a piece of state and a reducer function. While an async operation is in flight, it shows a "pending" version of the state derived by your reducer. Once the operation completes (success or error), it reverts to the underlying state — which by then should reflect the server's confirmed response, or the original value if something went wrong.
tsximport { useOptimistic, useTransition } from 'react'; type Message = { id: string; text: string; sending?: boolean }; function MessageList({ messages }: { messages: Message[] }) { const [optimisticMessages, addOptimistic] = useOptimistic( messages, (current, newText: string) => [ ...current, { id: crypto.randomUUID(), text: newText, sending: true }, ] ); const [isPending, startTransition] = useTransition(); async function handleSend(text: string) { startTransition(async () => { addOptimistic(text); await sendMessage(text); // server call }); } return ( <ul> {optimisticMessages.map((msg) => ( <li key={msg.id} style={{ opacity: msg.sending ? 0.6 : 1 }}> {msg.text} </li> ))} </ul> ); }
The sending: true flag gives you something to style against — a lighter opacity, a spinner, a "Sending..." label — without any additional state variables.
useOptimistic was designed to pair with Server Actions in Next.js. A Server Action is an async function that runs on the server but can be called like a local function from a Client Component. The combination is powerful:
tsx'use client'; import { useOptimistic } from 'react'; import { toggleLike } from './actions'; // Server Action function LikeButton({ postId, initialLiked }: { postId: string; initialLiked: boolean }) { const [optimisticLiked, setOptimisticLiked] = useOptimistic(initialLiked); return ( <form action={async () => { setOptimisticLiked(!optimisticLiked); await toggleLike(postId); }} > <button type="submit"> {optimisticLiked ? 'Unlike' : 'Like'} </button> </form> ); }
Notice setOptimisticLiked is called with a value directly here — when no reducer is needed, useOptimistic accepts a simple setter. The form's action attribute accepts an async function in React 19, which keeps the progressive-enhancement story intact.
The rollback behavior is built in. When the async operation wrapped in a transition throws or rejects, React reverts optimisticMessages (or whatever your optimistic state is) to the last confirmed value automatically. You do not need to catch the error and manually reset state.
What you should do is handle the error visibly:
tsxasync function handleSend(text: string) { startTransition(async () => { addOptimistic(text); try { await sendMessage(text); } catch { // useOptimistic reverts automatically. // Show a toast or error message here. showToast('Message failed to send. Try again.'); } }); }
The optimistic state disappears and the user sees the toast — a clean, honest failure experience.
A few constraints are worth internalizing before you reach for useOptimistic everywhere:
useTransition or a form action). Calling addOptimistic outside one is a no-op.revalidatePath or revalidateTag inside your Server Action so the page data refreshes.Not every mutation warrants an optimistic update. Before reaching for this pattern, consider:
For most additive, low-risk mutations — likes, follows, message sends, cart additions — optimistic updates are the right call.
Optimistic updates occupy a specific place in the state hierarchy. They are transient, derived state: always computed from the server truth, never the source of it. This is why they integrate well with local vs global state patterns — you do not put optimistic state in a global store. It lives in the component closest to the mutation.
If you are building features that involve mutations and loading states across a complex interface, also read up on error boundaries in React — catching render errors from failed state transitions is the complementary concern on the error-handling side.
useOptimistic is a focused, composable primitive. It does one thing: let you show a speculative UI state during an async operation and clean it up when the operation settles. No extra libraries, no complex reducer boilerplate, no manual rollback logic. For mutations that hit your Next.js API routes, pairing it with Server Actions gives you a clean, full-stack pattern that is both fast and honest about failure.