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'sformAction.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.useFormStatuslets a submit button know the form is pending.useOptimisticshows 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
TypeScript satisfies Explained: Check a Type Without Losing Precision
The satisfies operator checks that a value matches a type while keeping the value's own, more specific type. How it differs from a type annotation and from as, the patterns where it shines (config objects, route maps, as const), and the mistakes to avoid.
TypeScript Enums vs Union Types: Which Should You Use?
TypeScript enums generate runtime code and have surprising behaviour; string literal unions and as const objects give the same safety with plain JavaScript. How each works, why erasableSyntaxOnly and Node's built-in type stripping reject enums, and a practical rule for choosing.