← Back to blog

useOptimistic

useOptimistic provides immediate UI feedback while an action runs in the background.

Example: ArchiveButton with Pending State

From app/dashboard/_components/ArchiveButton.tsx:

'use client'; import { useOptimistic } from 'react'; export function ArchiveButton({ slug, archived }) { const [optimisticArchived, setOptimisticArchived] = useOptimistic(archived); const isPending = optimisticArchived !== archived; return ( <form data-pending={isPending || undefined} action={async () => { // Use updater function to read current pending state let newValue; setOptimisticArchived(current => { newValue = !current; return newValue; }); await toggleArchivePost(slug, newValue); }} > <button type="submit"> {optimisticArchived ? 'Unarchive' : 'Archive'} </button> </form> ); }

Why Use Updater Functions?

When users click rapidly, multiple actions queue up. Each closure captures the same optimisticArchived value:

// ❌ Stale closure - both clicks see archived=false setOptimisticArchived(!optimisticArchived); // false → true setOptimisticArchived(!optimisticArchived); // false → true (stale!) // ✅ Updater function reads from React's queue setOptimisticArchived(current => !current); // false → true setOptimisticArchived(current => !current); // true → false

Styling Parent Elements with CSS :has()

Expose pending state via data-pending attribute. Parent Server Components can style based on this using Tailwind's has-data-pending: variant:

// PostList.tsx (Server Component - no 'use client' needed!) <Card className="has-data-pending:animate-pulse has-data-pending:bg-muted/70"> <ArchiveButton slug={post.slug} archived={post.archived} /> </Card>

How It Works

  1. User submits the form
  2. setOptimisticArchived immediately updates the UI and sets data-pending
  3. CSS :has([data-pending]) triggers parent styles (pulse animation)
  4. The Server Function runs in the background
  5. When complete, the real archived prop replaces the optimistic value
  6. data-pending is removed, styles revert

Important: Requires Action Context

The optimistic setter must be called inside an Action—a function passed to an action prop or wrapped in startTransition. Form action props are automatically called inside startTransition.

Use for actions with high success rates: toggles, likes, bookmarks.

March 5, 2026297 words