React useOptimistic — Instant UI Feedback with Optimistic Updates

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Introduction

Why This Matters

Users expect instant feedback. When they click "Like" or add an item to a cart, a 300ms server round-trip feels slow. useOptimistic solves this by updating the UI immediately, then reconciling with the actual server response — or rolling back on failure. This pattern is sometimes called "optimistic concurrency."

Basic Syntax

const [optimisticState, addOptimistic] = useOptimistic(
  actualState,         // The real state from the server / parent
  (currentState, optimisticValue) => newState  // Optional reducer
)

When addOptimistic(value) is called, React immediately applies the optional reducer to produce a new display state. Once the pending async operation settles, React reverts to actualState (now updated by the server response).

Todo List Example

'use client'
 
import { useOptimistic, useState } from 'react'
 
type Todo = { id: number; text: string; pending?: boolean }
 
export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
  const [todos, setTodos] = useState<Todo[]>(initialTodos)
 
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo: Todo) => [...state, newTodo]
  )
 
  async function handleAddTodo(formData: FormData) {
    const text = formData.get('todo') as string
    const optimistic: Todo = { id: Date.now(), text, pending: true }
 
    addOptimisticTodo(optimistic)
 
    try {
      const res = await fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify({ text })
      })
      const saved: Todo = await res.json()
      setTodos(prev => [...prev, saved])
    } catch {
      // optimistic update reverts automatically when the action settles
      // but we must reset todos to not include the failed item
      setTodos(prev => prev) // no change — optimistic layer reverts
    }
  }
 
  return (
    <form action={handleAddTodo} className="space-y-2">
      {optimisticTodos.map(todo => (
        <div key={todo.id} className={todo.pending ? 'opacity-50' : ''}>
          {todo.text}
          {todo.pending && <span className="ml-2 text-xs text-gray-400">saving...</span>}
        </div>
      ))}
      <input name="todo" className="border px-2 py-1 rounded" required />
      <button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded">
        Add Todo
      </button>
    </form>
  )
}

Social Feed: Like Button

One of the most common optimistic update patterns is a like/upvote button.

'use client'
 
import { useOptimistic } from 'react'
import { toggleLike } from '@/app/actions'
 
type LikeState = { liked: boolean; count: number }
 
export function LikeButton({ postId, initialLiked, initialCount }: {
  postId: string
  initialLiked: boolean
  initialCount: number
}) {
  const [optimisticLike, updateOptimisticLike] = useOptimistic(
    { liked: initialLiked, count: initialCount },
    (state: LikeState): LikeState => ({
      liked: !state.liked,
      count: state.liked ? state.count - 1 : state.count + 1
    })
  )
 
  async function handleLike() {
    updateOptimisticLike(undefined) // reducer handles the toggle
    await toggleLike(postId)
  }
 
  return (
    <button
      onClick={handleLike}
      className={`flex items-center gap-2 px-3 py-1 rounded-full transition-colors ${
        optimisticLike.liked
          ? 'bg-red-100 text-red-600'
          : 'bg-gray-100 text-gray-600'
      }`}
    >
      {optimisticLike.liked ? 'Liked' : 'Like'} {optimisticLike.count}
    </button>
  )
}

Shopping Cart Quantity

'use client'
 
import { useOptimistic } from 'react'
import { updateCartQuantity } from '@/app/actions'
 
export function CartItem({ item }: { item: { id: string; name: string; price: number; quantity: number } }) {
  const [optimisticQty, setOptimisticQty] = useOptimistic(item.quantity)
 
  async function handleChange(newQty: number) {
    if (newQty &lt; 1) return
    setOptimisticQty(newQty)
    await updateCartQuantity(item.id, newQty)
  }
 
  return (
    <div className="flex items-center gap-4 p-4 border rounded-lg">
      <span className="flex-1 font-medium">{item.name}</span>
      <div className="flex items-center gap-2">
        <button
          onClick={() => handleChange(optimisticQty - 1)}
          className="w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300"
        >
          -
        </button>
        <span className="w-8 text-center font-semibold">{optimisticQty}</span>
        <button
          onClick={() => handleChange(optimisticQty + 1)}
          className="w-8 h-8 rounded-full bg-gray-200 hover:bg-gray-300"
        >
          +
        </button>
      </div>
      <span className="font-semibold">${(item.price * optimisticQty).toFixed(2)}</span>
    </div>
  )
}

Common Mistakes

  • Not handling rollback on error — if the server fails, useOptimistic reverts automatically, but you must ensure the real state does not include the failed item
  • Using useOptimistic without a pending visual indicator — users should know the action is in-flight
  • Applying optimistic updates to destructive actions like "Delete Account" — use confirmation dialogs for irreversible operations
  • Forgetting that useOptimistic is a Client Component feature — you need 'use client'

Best Practices

  • Always show a visual "pending" state (opacity, spinner, or label) on optimistically updated items
  • Use optimistic updates only for lightweight, reversible actions (likes, quantity changes, toggles)
  • Pair with Server Actions for the cleanest code — the form's action prop handles pending state automatically
  • Add error toasts or notifications when server operations fail so users know the action did not persist

Key Takeaways

  • useOptimistic is a React 19 hook that shows an optimistic UI immediately while an async operation runs
  • The hook takes actual state and an optional reducer; the optimistic state reverts once the async action settles
  • Call addOptimistic(value) inside an async event handler or Server Action to trigger the optimistic update
  • pending: true flags on optimistic items let you style in-flight updates differently (e.g., greyed out)
  • This pattern eliminates the need for manual loading states on frequent actions like likes and cart updates
  • useOptimistic is specifically designed for use with React Server Actions and the form action prop
  • Always test failure paths — verify the UI correctly reflects reality when server operations fail
  • Irreversible or high-stakes actions should use confirmation dialogs, not optimistic updates

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading