Blog
3 min read

React useActionState and useOptimistic: Forms Without the Boilerplate

React 19's Actions replace hand-rolled loading and error state for forms. How useActionState returns state, a form action and isPending; how useOptimistic shows a change instantly and rolls back on failure; useFormStatus for submit buttons; and how they fit with Next.js server actions.

Before React 19, every form needed the same boilerplate: useState for loading, useState for errors, an onSubmit handler with preventDefault, a try/catch, and resetting it all. React 19's Actions — and two hooks, useActionState and useOptimistic — remove most of it.

Actions in one sentence

You can pass an async function to a form's action prop. React calls it with the form's FormData, tracks whether it's pending, and resets uncontrolled form fields when it succeeds.

<form action={async (formData) => { await saveName(formData.get('name')) }}>
  <input name="name" />
  <button>Save</button>
</form>

useActionState: result, action, pending

import { useActionState } from 'react'

async function subscribe(prevState, formData) {
  const email = formData.get('email')
  if (!email.includes('@')) return { error: 'Enter a valid email' }
  const res = await fetch('/api/subscribe', { method: 'POST', body: formData })
  if (!res.ok) return { error: 'Something went wrong, try again' }
  return { success: true }
}

export function NewsletterForm() {
  const [state, formAction, isPending] = useActionState(subscribe, {})

  return (
    <form action={formAction}>
      <input name="email" type="email" />
      <button disabled={isPending}>{isPending ? 'Subscribing…' : 'Subscribe'}</button>
      {state.error && <p role="alert">{state.error}</p>}
      {state.success && <p>You're in!</p>}
    </form>
  )
}

useActionState(action, initialState) gives you:

  • state — whatever your action last returned (start: initialState).
  • formAction — pass this to <form action> or a button's formAction.
  • isPending — true while it runs.

Your action receives the previous state first, then the FormData. Return errors as data rather than throwing, so the UI can show them.

useFormStatus: a reusable submit button

Inside a form, a child component can read the form's status without props:

import { useFormStatus } from 'react-dom'

function SubmitButton({ children }) {
  const { pending } = useFormStatus()
  return <button disabled={pending}>{pending ? 'Saving…' : children}</button>
}

useOptimistic: show it before the server confirms

For actions that almost always succeed — liking a post, adding a comment, ticking a todo — waiting for the server feels slow. useOptimistic shows the expected result immediately and automatically falls back to the real state when the action finishes.

import { useOptimistic } from 'react'

export function Comments({ comments, addComment }) {
  const [optimisticComments, addOptimistic] = useOptimistic(
    comments,
    (current, newText) => [...current, { id: 'temp', text: newText, sending: true }],
  )

  async function action(formData) {
    const text = formData.get('text')
    addOptimistic(text)       // appears instantly
    await addComment(text)    // real save; parent updates `comments`
  }

  return (
    <>
      <ul>
        {optimisticComments.map(c => (
          <li key={c.id} style={{ opacity: c.sending ? 0.5 : 1 }}>{c.text}</li>
        ))}
      </ul>
      <form action={action}>
        <input name="text" />
        <button>Post</button>
      </form>
    </>
  )
}

If addComment fails, the optimistic item disappears when the action ends, because comments never changed. Show an error so the user knows.

With Next.js server actions

These hooks pair naturally with server actions: the function you pass to useActionState can be a 'use server' function, so the form works end-to-end without writing an API route. Validate input on the server — the client can send anything. (Next.js Server Actions, Validate Input With Zod)

When to still use a form library

For large forms with field-level validation, dependent fields and complex UX, a library like React Hook Form is still worth it. Actions shine for small and medium forms. (React Hook Form)

The summary

  • useActionState = action result + form action + pending flag, no manual state.
  • useFormStatus lets a submit button know the form is pending.
  • useOptimistic shows changes instantly and reverts on failure.

EasySpawn runs your React app and its backend on one server, so form actions hit a real database over a local connection — and Claude Code can refactor old forms to Actions for you. See how it works or join the waitlist.

Related: Form Validation Explained · React useState Explained · TanStack Query vs useEffect · React Server Components Explained

Keep reading