React useActionState Hook for Form Handling
Learn how to use React's useActionState hook to manage form submissions, pending states, and server responses with clean, declarative code.
What you'll learn
- ✓What useActionState does and why it exists
- ✓How to handle form submissions with pending states
- ✓Combining useActionState with Server Functions
- ✓Error handling and validation patterns
- ✓Migrating from useFormState to useActionState
Prerequisites
- •Basic React knowledge
- •Familiarity with React hooks
React 19 introduced useActionState, a hook that simplifies form handling by combining the action, the result state, and the pending indicator into a single call. If you have ever juggled useState, useTransition, and manual error tracking just to submit a form, this hook will feel like a relief.
The Problem useActionState Solves
Traditional form handling in React requires multiple pieces of state. You need a variable for the submission result, another for the loading flag, and often a third for errors. The logic scatters across event handlers, effects, and render conditions. useActionState collapses all of that into one hook.
Basic Syntax
The hook accepts an action function and an initial state. It returns the current state, a wrapped action you pass to the form, and a boolean indicating whether the action is still running.
import { useActionState } from 'react';
async function submitForm(previousState, formData) {
const name = formData.get('name');
if (!name) {
return { error: 'Name is required' };
}
await saveToDatabase({ name });
return { success: true, error: null };
}
function SignupForm() {
const [state, formAction, isPending] = useActionState(submitForm, {
error: null,
success: false,
});
return (
<form action={formAction}>
<input name="name" placeholder="Your name" />
{state.error && <p className="error">{state.error}</p>}
<button disabled={isPending}>
{isPending ? 'Submitting...' : 'Sign Up'}
</button>
{state.success && <p>Welcome aboard!</p>}
</form>
);
}
Notice how the form uses the native action attribute instead of an onSubmit handler. React intercepts it, calls your action with the previous state and the FormData object, and updates the state when it resolves.
Understanding the Action Function
The action function receives two arguments. The first is the previous state, which is whatever the action returned last time (or the initial state on the first call). The second is the FormData from the submission.
async function loginAction(prev, formData) {
const email = formData.get('email');
const password = formData.get('password');
try {
const user = await authenticate(email, password);
return { user, error: null };
} catch (err) {
return { user: null, error: err.message };
}
}
Returning plain objects keeps the action pure and testable. You can unit-test it without rendering a component at all.
Validation Before Submission
Client-side validation fits naturally inside the action. Because the action is async, you can mix synchronous checks with server-round-trip validation.
async function createPostAction(prev, formData) {
const title = formData.get('title')?.toString().trim();
const body = formData.get('body')?.toString().trim();
const errors = {};
if (!title) errors.title = 'Title is required';
if (!body) errors.body = 'Body is required';
if (title && title.length < 5) errors.title = 'Title must be at least 5 characters';
if (Object.keys(errors).length > 0) {
return { errors, success: false };
}
const post = await api.createPost({ title, body });
return { errors: {}, success: true, postId: post.id };
}
function CreatePostForm() {
const [state, action, isPending] = useActionState(createPostAction, {
errors: {},
success: false,
});
return (
<form action={action}>
<div>
<input name="title" placeholder="Post title" />
{state.errors.title && <span>{state.errors.title}</span>}
</div>
<div>
<textarea name="body" placeholder="Write something..." />
{state.errors.body && <span>{state.errors.body}</span>}
</div>
<button disabled={isPending}>
{isPending ? 'Creating...' : 'Create Post'}
</button>
</form>
);
}
Using with Server Functions
When paired with Server Functions ("use server"), useActionState enables progressive enhancement. The form works even before JavaScript loads on the client.
// actions.js
'use server';
export async function subscribeAction(prev, formData) {
const email = formData.get('email');
const result = await db.subscribers.insert({ email });
if (result.error) {
return { message: result.error, ok: false };
}
return { message: 'Subscribed!', ok: true };
}
// SubscribeForm.jsx
'use client';
import { useActionState } from 'react';
import { subscribeAction } from './actions';
export function SubscribeForm() {
const [state, action, isPending] = useActionState(subscribeAction, {
message: '',
ok: false,
});
return (
<form action={action}>
<input type="email" name="email" required />
<button disabled={isPending}>Subscribe</button>
{state.message && (
<p className={state.ok ? 'success' : 'error'}>{state.message}</p>
)}
</form>
);
}
Resetting State After Success
A common need is to clear the form after a successful submission. You can use a key to remount the form, or reset specific fields through a ref.
function ContactForm() {
const [state, action, isPending] = useActionState(contactAction, {
sent: false,
error: null,
});
const formRef = useRef(null);
useEffect(() => {
if (state.sent) {
formRef.current?.reset();
}
}, [state.sent]);
return (
<form ref={formRef} action={action}>
<input name="message" />
<button disabled={isPending}>Send</button>
</form>
);
}
Migration from useFormState
If you used the earlier useFormState from react-dom, the migration is straightforward. The API is almost identical, but useActionState now lives in react (not react-dom) and adds the third isPending return value, eliminating the need for a separate useFormStatus call in many cases.
// Before
import { useFormState } from 'react-dom';
const [state, action] = useFormState(myAction, initialState);
// After
import { useActionState } from 'react';
const [state, action, isPending] = useActionState(myAction, initialState);
When to Reach for useActionState
Use this hook when you have a form whose submission triggers an async operation and you want the result to live alongside the form. For forms that only update local state synchronously, plain useState is still fine. For global mutations that affect many parts of the page, consider pairing useActionState with a cache invalidation strategy like revalidatePath in Next.js or a query client refetch.
The hook keeps form logic contained, testable, and progressively enhanced. It is one of the most practical additions in React 19.
Related articles
- React React Forms: Controlled vs Uncontrolled Inputs Explained
Understand the difference between controlled and uncontrolled inputs in React, when to use each, and how to combine them with refs and form libraries.
- React React Custom Hooks: Patterns for Reusable Logic
Master React custom hooks with practical patterns for data fetching, form handling, debouncing, and more. Learn when and how to extract reusable logic.
- React React Form Handling: Controlled, Uncontrolled, and Libraries
Master React forms with controlled and uncontrolled inputs, validation patterns, and React Hook Form. Build accessible, performant forms step by step.
- React React Higher-Order Components vs Hooks: Migration Guide
Compare Higher-Order Components and hooks in React with side-by-side examples, migration strategies, and guidance on when each pattern still makes sense.