useOptimistic in AI-Native Apps: Instant Feedback While the LLM Thinks
React 19's useOptimistic hook lets you show a temporary UI state immediately — before the server responds. Here's how to use it in AI-native Next.js apps to make LLM interactions feel instant.
June 15, 2026 · 5 min read
One real cost of LLM integrations is latency. Even fast models take 500ms before the first token arrives. If you wait for the server to respond before updating the UI, every interaction feels sluggish — the user types a message, clicks Send, and stares at a spinner. React 19's useOptimistic hook is the fix.
What useOptimistic does
useOptimistic lets you show a temporary UI state immediately — before the Server Function has responded. When the action completes, React replaces the temporary state with the real server result. If the action throws, it rolls back automatically. No cleanup logic, no manual state reconciliation.
The hook takes two arguments and returns two values:
const [optimisticState, addOptimistic] = useOptimistic(state, updateFn)
state is your real state — the list of messages returned by the server. updateFn is a pure function (currentState, optimisticValue) => newState that merges in the temporary value. addOptimistic(value) triggers the merge synchronously inside a transition — the UI updates before the round-trip starts.
Chat example: message appears before the LLM replies
The canonical AI use case is a chat interface. The user sends a message; you want it to appear immediately while the model generates a reply. Here is the full Client Component:
'use client'
import { useActionState, useOptimistic } from 'react'
import { sendMessage } from '@/lib/actions'
type Message = { role: 'user' | 'assistant'; content: string }
export function ChatThread({ initialMessages }: { initialMessages: Message[] }) {
const [messages, dispatch, pending] = useActionState(sendMessage, initialMessages)
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(current, newContent: string) => [
...current,
{ role: 'user' as const, content: newContent },
]
)
return (
<div>
{optimisticMessages.map((m, i) => (
<div key={i} data-role={m.role}>
{m.content}
</div>
))}
<form
action={async (formData) => {
const content = formData.get('message') as string
addOptimistic(content)
await dispatch(formData)
}}
>
<input name="message" required />
<button type="submit" disabled={pending}>Send</button>
</form>
</div>
)
}The addOptimistic call fires synchronously before dispatch awaits the server. The message appears in the thread immediately. When sendMessage resolves and returns the full updated thread — now including the assistant reply — React swaps the optimistic state for the real one in a single commit.
The Server Function
The action appends the user message, calls the LLM, and returns the complete updated thread. Because useActionStateexpects the action's first argument to be the previous state, the signature is slightly different from a plain Server Function:
'use server'
import { generateText } from 'ai'
import { openai } from '@ai-sdk/openai'
type Message = { role: 'user' | 'assistant'; content: string }
export async function sendMessage(
currentMessages: Message[],
formData: FormData
): Promise<Message[]> {
const content = formData.get('message') as string
const updated = [...currentMessages, { role: 'user' as const, content }]
const { text } = await generateText({
model: openai('gpt-4o-mini'),
messages: updated,
})
return [...updated, { role: 'assistant' as const, content: text }]
}The API key stays on the server. The database write, LLM call, and state update all happen in one function. The component never fetches anything directly.
Automatic rollback
If sendMessage throws — network failure, rate limit, invalid input — React reverts to the state before addOptimistic was called. The temporary message disappears; the input value is still in the form. You do not write rollback logic. Surface the error through the state returned by useActionState or an error boundary wrapping the component.
When to use it and when to skip it
Use useOptimisticfor low-risk writes: sending a chat message, toggling a like, appending a tag. The optimistic value you show should be something you already know — the user's own input — not a value you are waiting for the server to compute.
Skip it for destructive operations where a premature success state causes real confusion: deleting a file, confirming a payment, removing a team member. Skip it when the displayed value depends on server-generated data — an AI-generated slug that must be unique, a word count computed server-side, a price that comes from a database. In those cases, show a loading state instead.
Combined with useActionState and Server Functions, useOptimistic gives AI chat interfaces the feel of a real-time app without a WebSocket or polling loop in sight.