Instant feedback with useOptimistic, derived pending state, data-pending for parent styling.
The best in-between state is no perceived delay at all. When a user toggles a setting, bookmarks an item, or archives a post, the UI should update instantly—even while the server action runs in the background.
useOptimistic creates a local state that updates immediately, then syncs with the actual server state when the action completes. If the action fails, React automatically reverts to the true state.
Here's an example using useOptimistic for instant archive toggle:
'use client';
import { useOptimistic, startTransition } from 'react';
export function ArchiveButton({ slug, archived }) {
const [optimisticArchived, setOptimisticArchived] = useOptimistic(archived);
const isPending = optimisticArchived !== archived;
return (
<form action={async () => {
startTransition(() => setOptimisticArchived(!optimisticArchived));
await toggleArchivePost(slug, !archived);
}}>
<button type="submit" disabled={isPending}>
{optimisticArchived ? 'Unarchive' : 'Archive'}
</button>
</form>
);
}Click archive → icon switches instantly → server updates in background → states sync.
Notice there's no separate isPending from useTransition. Instead, we derive it:
const isPending = optimisticArchived !== archived;While the action runs, the optimistic value differs from the prop. When the action completes and the parent re-renders with new data, they match again and isPending becomes false. This is elegant—the pending state emerges from the data itself.
The data-pending attribute enables something powerful: Server Components can style based on Client Component state.
// In the Client Component - add data attribute based on pending
<form data-pending={isPending || undefined}>
// In the Server Component (PostList)
<Card className="has-data-pending:animate-pulse has-data-pending:bg-muted/70">
<ArchiveButton slugTailwind's has-[[data-pending]]: variant uses CSS :has([data-pending])—the Card pulses during the action without becoming a Client Component itself.
Best for high success rate, reversible actions:
Avoid for actions that need validation feedback or have meaningful failure modes—use useTransition instead so you can handle errors before updating UI.