Updater functions for rapid clicks, data-pending for parent styling, ArchiveButton pattern.
useOptimistic provides immediate UI feedback while an action runs in the background.
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>
);
}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
setOptimisticArchivedExpose 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}setOptimisticArchived immediately updates the UI and sets data-pending:has([data-pending]) triggers parent styles (pulse animation)archived prop replaces the optimistic valuedata-pending is removed, styles revertThe 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.