Module P-5·24 min read

Setting up Prisma with the App Router, why new PrismaClient() per request will kill your database at scale, PgBouncer and Neon for serverless pooling, and the full mutation pattern via Server Actions.

Optimistic UI: useOptimistic, useActionState, and the Pending State Pattern

Who this module is for: You have Server Actions working — the form submits, the data saves, the page updates. But the UI feels sluggish: users click a button and stare at a spinner while the server round-trip completes. This module covers the three React 19 hooks that make Server Actions feel instant — useOptimistic for immediate UI updates, useActionState for form state management, and the pending state pattern for visual feedback.


Why Optimistic UI Matters

A Server Action invokes a round-trip to your server: the browser sends a request, Next.js executes the action, revalidates the cache, and the updated UI flows back down. Even on a fast connection, this takes 200–800ms. On a mobile network, 1–3 seconds.

Users have been conditioned by native apps and real-time UIs to expect instant feedback. A button that does nothing for 500ms feels broken. Optimistic UI solves this by updating the UI immediately — before the server confirms — and rolling back if the server reports an error.

The pattern has three layers:

  1. Pending state — show that something is happening (spinner, disabled state)
  2. Optimistic update — show the expected result immediately
  3. Rollback — revert if the server returns an error

React 19 provides a hook for each layer.


useActionState: Form State Management

useActionState (formerly useFormState in React 18) connects a Server Action to a component's state. It gives you the action's return value and a wrapped action function that you pass to a form.

tsx
typescript

What useActionState gives you:

  • state — the most recent return value from the Server Action (or initial state on first render)
  • action — a wrapped version of your Server Action to pass to <form action={action}>
  • isPendingtrue while the action is in flight

The signature change: your Server Action must accept prevState as the first argument before formData. The prevState is the previous return value — useful for accumulating state across multiple submissions.


useOptimistic: Instant UI Updates

useOptimistic lets you show a temporary "optimistic" value while an async operation is pending, then automatically revert to the actual value when the operation completes.

tsx

How useOptimistic Works Internally

useOptimistic takes two arguments:

  1. The real state (synced with the server)
  2. An update function: (currentState, optimisticValue) => newState

When you call addOptimistic(value):

  • The component immediately re-renders with updateFn(currentState, value)
  • While the associated async operation is pending, React shows the optimistic state
  • When the async operation completes (or if the component re-renders with new real state), the optimistic state is discarded and the real state takes over

The rollback is automatic — if your Server Action throws, React reverts the optimistic update because the pending state resolves.


Combining All Three: A Complete Example

The canonical pattern: useActionState for form management + useOptimistic for immediate list updates:

tsx

The temporary items render with reduced opacity and a "(saving...)" tag. When the server responds, setTodos(result.todos) updates the real state with the server-assigned IDs, and the optimistic overlay is replaced.


The Pending State Pattern

Beyond spinners, a well-designed pending state communicates exactly what is happening and prevents double-submission.

Disable the Trigger

Always disable the submit button (or the entire form) while isPending is true:

tsx

Skeleton vs Spinner

For content that takes > 300ms, a skeleton is better UX than a spinner. Use Suspense boundaries with skeleton fallbacks for the loading state:

tsx

useFormStatus for Deep Components

useFormStatus (from react-dom) reads the pending state of the nearest parent <form> — useful for submit buttons nested inside form components:

tsx

Error Handling and Rollback Patterns

Explicit Rollback with Error Boundaries

For operations that must roll back cleanly (financial, irreversible):

tsx

Toasts for Async Feedback

Optimistic UI removes the "loading" state, but users still need to know when something fails:

tsx

When NOT to Use Optimistic UI

Optimistic UI is a UX enhancement, not a correctness guarantee. Do not use it when:

  • The operation is not idempotent — if showing the wrong result before server confirmation causes confusion (e.g., payment confirmations, inventory deduction)
  • Rollback is disorienting — if the rollback experience is worse than waiting (e.g., the user navigated away expecting success)
  • The operation is fast — if the server responds in < 100ms, optimistic UI adds complexity with no UX benefit
  • You need the server response to continue — if the UI flow depends on a server-assigned ID or token, you must wait for the response

The useTransition Alternative

For non-form async operations (button clicks that trigger state transitions), useTransition provides isPending without the form machinery:

tsx

useTransition marks the state update as non-urgent — React can interrupt it if higher-priority updates arrive (user input). Useful for transitions that should not block typing or other interactions.


Summary

  • useActionState — connects a Server Action to component state; gives you state, action, and isPending; Server Action signature gains prevState as first param
  • useOptimistic — shows an immediate temporary value; auto-reverts to real state when the async operation completes; rollback is automatic on error
  • useFormStatus — reads pending state from the nearest parent form; avoids prop-drilling isPending into nested submit buttons
  • useTransition — marks async state updates as non-urgent; provides isPending for non-form button interactions
  • The pattern: optimistic update → async server call → sync real state on success → auto-rollback on failure
  • When to skip it: payment flows, irreversible operations, fast operations (< 100ms), flows that depend on server-assigned values

Next: P-4 — Authentication with Auth.js v5 — session management, OAuth providers, database sessions, and protecting routes with middleware.


Knowledge Check

What hook from React 19 is used for managing the state, pending status, and return value of a Server Action in a form?


How does useOptimistic handle an error if the server request fails?


When should you typically NOT use optimistic UI updates?

Test your knowledge with more question sets

Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.