Optimistic UI Updates: Chat Prompts
Optimistic updates make your app feel fast by applying mutations to the UI before the server confirms them. Hit the like button → counter increments instantly → server request happens in the background → if it fails, the counter rolls back. Implemented well, optimistic UI is the difference between an app that feels snappy and one that feels laggy. Implemented poorly, it's the difference between an app users trust and one where they constantly wonder "did that save?"
The hard parts aren't the happy path. They're: rollback when the server rejects the mutation, reconciling the optimistic value with the canonical server response (which may differ — server-assigned IDs, computed fields, server timestamp), conflict resolution when another client wrote in between, and queueing dependent mutations (delete a parent before the child create returns).
When Optimistic Updates Make Sense
Use optimistic UI for mutations where:
- Failure is rare — toggling a like, marking complete, renaming an item. The server almost always says yes.
- Rollback is cheap — reverting the value is straightforward; no destructive side effects already occurred.
- Latency hides badly — anything triggered by user interaction in the main flow (clicks, typing) where waiting feels broken.
Avoid optimistic UI for:
- Payments, irreversible actions, or anything where pretending it succeeded misleads the user
- Mutations with complex server-side validation that frequently fails
- Anything where the "real" result is meaningfully different from the optimistic prediction (server-side computed price, etc.)
TanStack Query Optimistic Mutation
The dominant pattern in 2026 React apps. TanStack Query's useMutation has built-in support for optimistic updates via onMutate / onError / onSettled.
I want to add an optimistic update for [mutation name, e.g. "toggle todo complete"] in my Next.js app using TanStack Query v5.
Context:
- Query key: [e.g. ['todos'] or ['todos', listId]]
- Server endpoint: [POST/PATCH/DELETE path]
- Mutation input: [shape of data sent]
- Server response: [shape returned, especially fields the client doesn't know like server-assigned id, updatedAt]
Implement:
1. `useMutation` with `mutationFn` calling [endpoint]
2. `onMutate(variables)`:
- `await queryClient.cancelQueries({ queryKey: [key] })` to prevent races with in-flight refetches
- Snapshot previous data: `const previous = queryClient.getQueryData([key])`
- Optimistically update cache: `queryClient.setQueryData([key], (old) => [...transformation...])`
- Return `{ previous }` for the rollback context
3. `onError(err, variables, context)`:
- Rollback: `queryClient.setQueryData([key], context.previous)`
- Show toast: [error message]
4. `onSettled()`:
- Invalidate to reconcile with server: `queryClient.invalidateQueries({ queryKey: [key] })`
Edge cases to handle:
- Multiple in-flight mutations to the same item — last-write-wins or queue?
- Server returns a different value than predicted (e.g. server-assigned id) — reconcile
- Network offline — should the mutation queue and retry?
Output:
- Hook: `useToggleTodo()` (or whichever)
- Usage example in a component
- Toast/error UI snippet
Optimistic Create with Server-Assigned ID
The trickiest pattern. You don't have the real id until the server responds, but you need to render the new item now.
I'm building an optimistic CREATE flow for [resource, e.g. "comments"] using TanStack Query.
The server assigns the `id`, `createdAt`, and `userId` (from session). The client sends `{content, parentId}`.
Implement an optimistic insert pattern:
1. Generate a temporary client-side id: `const tempId = 'temp-' + crypto.randomUUID()`
2. Insert optimistically with that tempId, status `pending`
3. On success, replace the tempId entry with the real server response (so subsequent edits/deletes target the real id)
4. On error, remove the tempId entry and surface the failure to the user
5. While `pending`, render the comment in a slightly muted state so users see it's not yet confirmed
Show me:
- The mutation hook with `onMutate`/`onSuccess`/`onError`
- How to render the pending state in the component (opacity, "sending..." indicator)
- How to handle a user trying to delete or edit an item that's still in `pending` state — disable the actions, or queue them?
- Code for queueing a child mutation (replying to a still-pending comment) until the parent confirms its real id
Stack: Next.js App Router + TanStack Query v5 + tRPC (or fetch — call out the difference).
Output: a complete useCreateComment hook with optimistic insert, server-id reconciliation, pending state UI, and dependent-mutation queueing.
Optimistic Update with Conflict Resolution
When two clients edit the same record, one of them must lose. Make that visible.
I have a [resource, e.g. "document title"] field that multiple users can edit. I want optimistic updates with conflict detection.
Context:
- Each [resource] has a `version` integer (incremented on every server-side write)
- Client sends `{newValue, expectedVersion}` on update
- Server returns 409 Conflict if `expectedVersion` doesn't match current
- 200 OK with `{value, version}` on success
Build the optimistic mutation:
1. `onMutate`: snapshot, optimistically apply with the new value (don't bump version yet)
2. On 200: replace optimistic value with server response (version is now updated)
3. On 409: keep the user's input visible, show a "conflict — somebody else updated this. Their version: [X]. Your version: [Y]. [Keep mine] [Take theirs] [Merge]" UI
4. On other errors (500, network): rollback and show retry
Provide:
- Hook code
- Conflict-resolution dialog component
- A "merge" implementation strategy when both edits are non-trivial (3-way merge if it's plain text; otherwise show diff)
Stack: Next.js App Router + TanStack Query v5.
Optimistic Delete with Undo
Pair every optimistic delete with an undo affordance — Gmail-style. Removes the "are you sure?" friction without losing forgiveness.
I want to implement optimistic delete with a 5-second undo window for [resource, e.g. "todos"].
UX flow:
1. User clicks delete → item disappears immediately from the list (optimistic)
2. Toast appears: "Todo deleted. [Undo]" with countdown
3. After 5 seconds, the actual server DELETE call fires
4. If user clicks Undo before the timer: restore the item, cancel the server call
5. If server call fails: re-insert the item, show error toast
Implementation strategy:
- Should the server call delay 5 seconds, or should we render-only-delete and call server later?
- I'm worried about: user navigates away mid-undo-window; user has multiple pending deletes; user closes the tab
- Show me: the hook, the toast component with countdown, the cleanup logic on navigation/unmount
Stack: Next.js App Router + TanStack Query v5 + sonner (or whichever toast library).
The right answer: render-only-delete immediately, debounce the actual server call by 5 seconds, cancel debounced call on undo. On navigate/close, fire the pending deletes immediately (don't lose them).
SWR Optimistic Mutation
SWR's mutate API is simpler than TanStack Query's. Pattern is mostly the same.
I'm using SWR (not TanStack Query) and want to add optimistic updates for [mutation].
Show me:
1. `mutate(key, optimisticData, { revalidate: false })` to apply the change immediately
2. Trigger the actual fetch
3. On success: `mutate(key)` to revalidate from server
4. On error: rollback by calling `mutate(key, previousData, { revalidate: false })`
Compare this to TanStack Query — when would I prefer SWR's pattern vs Query's?
My specific mutation: [describe]. My data shape is: [describe].
Optimistic Update Across Multiple Queries
A single mutation often invalidates several queries. A new comment changes:
- The comments list for that thread
- The thread's
commentCounton the listing page - The user's "your activity" feed
Don't only update the obvious one — update them all, or invalidate them all.
I have a mutation that affects multiple cached queries. When user posts a new [resource] in [parent], I need to update:
1. `['comments', threadId]` — append the new comment optimistically
2. `['threads', threadId]` — increment commentCount
3. `['user-activity', userId]` — prepend an activity entry
4. Possibly: `['notifications', mentionedUserId]` — add a mention notification (or wait for server-side fanout?)
Build the `onMutate` that snapshots and updates all of these atomically, and the `onError` that rolls them all back.
Two design questions I want you to address:
- Should I `setQueryData` on each one (manual update), or `invalidateQueries` afterward (refetch)? Tradeoff?
- For data the client can't predict (like notification ids), should I optimistically update at all, or wait for the server?
Stack: TanStack Query v5.
The pragmatic answer: optimistically update only the queries where the user expects an immediate visual change (the comment list, the count). For everything else (notifications, server-driven analytics), invalidate on settle and let it refetch quietly.
Network-Aware Optimistic Updates
Online: optimistic + server confirms. Offline: optimistic + queue + retry on reconnect. This is the "real PWA" tier of polish.
My app needs to support offline optimistic updates. When the user edits [resource] without connection:
1. Apply optimistically (visible to user)
2. Queue the mutation
3. When connection returns, replay queued mutations in order
4. If a queued mutation fails (conflict, 401, etc.), surface to user
Implement using:
- `navigator.onLine` for connection detection
- A persistent queue in IndexedDB (so offline edits survive a tab close)
- TanStack Query's `mutationCache` + custom persister, or a hand-rolled queue
Show me:
- The queue data structure
- The replay logic (FIFO? merge similar mutations? skip stale ones?)
- The reconnection handler
- The UI: a banner showing "You're offline. X changes will sync when you reconnect"
Stack: Next.js + TanStack Query v5 + IndexedDB (idb library) + service worker (workbox?).
Optimistic Update with Form State
Forms with optimistic submit are subtle. The form's local state and the server's canonical state can diverge if the user keeps typing during the round trip.
I have a settings form (name, bio, avatar). On save:
1. Apply changes optimistically to all places the user's profile is rendered (header, card, sidebar)
2. Send PATCH to server
3. Server may modify the values (e.g. trim whitespace, generate avatar URL from upload)
4. Reconcile: replace optimistic values with server response
UX challenge: what if the user edits the form *again* while the first save is still in flight?
- Block the form? (unfriendly)
- Queue the second save? (race conditions)
- Apply both optimistically, last-write-wins on server response? (data loss possible)
Show me:
- The mutation hook handling concurrent form submits
- The form component pattern that disables save during in-flight save (or queues it)
- A subtle in-flight indicator (button spinner, not a blocking modal)
Stack: Next.js + TanStack Query v5 + react-hook-form.
Common Pitfalls
Forgetting cancelQueries: if a refetch is in-flight when your mutation lands, the refetch can overwrite your optimistic update with the old server data. Always cancel before applying.
Not snapshotting the previous data: rollback needs previous from onMutate. Save it; pass it through context to onError.
Optimistic updates with server-side side effects: if the server creates a webhook, sends an email, or charges a card, optimistic UI lying about "done" is a lie users will catch. Use loading state instead.
Keeping the optimistic value after server response: always reconcile. Server-assigned IDs, computed fields, and timestamps need to flow back into the cache or your client and server diverge silently.
Dependent mutations on temp IDs: if user creates item A, then immediately creates child B referencing A's temp ID, B's request hits the server before A finishes. Either (1) queue B until A returns, or (2) have the server accept temp IDs and resolve them.
Toast spam on rollback: if 10 mutations fail at once, don't show 10 toasts. Group them ("3 changes failed to save").
Offline "optimistic forever": if you let users edit offline and never warn them, they'll write a 2000-word doc that fails to save with a conflict on reconnect. Show offline status prominently.