Actions & useActionState
React 19 Actions handle form submission, pending state and results in one hook.
Syntax
const [state, formAction, pending] = useActionState(action, initial);React 19 introduces Actions: functions you pass to a form's action prop to handle submission, including async work. The useActionState hook wires an action to state and a pending flag.
What useActionState gives you
- The latest state returned by your action (e.g. a result or error message).
- A wrapped action to pass to
<form action={...}>. - A pending boolean that is true while the action runs.
Your action receives the previous state and the submitted FormData, and returns the next state.
Example
import { useActionState } from 'react';
async function submit(prev, formData) {
const name = formData.get('name');
await new Promise((r) => setTimeout(r, 500));
return { message: 'Hello, ' + name };
}
function NameForm() {
const [state, formAction, pending] = useActionState(submit, { message: '' });
return (
<form action={formAction}>
<input name="name" />
<button disabled={pending}>{pending ? 'Saving...' : 'Save'}</button>
<p>{state.message}</p>
</form>
);
}When to use it
- A comment form uses useActionState so the pending flag disables the submit button automatically while the server processes the POST, preventing double-submissions.
- A login form wires its server action through useActionState and displays the returned error message (e.g., 'Invalid credentials') without managing a separate error state variable.
- A file-upload form uses an async action with useActionState to show an in-progress indicator and then the server response β all without writing fetch boilerplate.
More examples
Basic useActionState form
Wires an async server action to a form, automatically tracking pending state and the returned result.
import { useActionState } from 'react';
async function submitAction(prevState, formData) {
const name = formData.get('name');
await new Promise(r => setTimeout(r, 800)); // simulate server
return `Hello, ${name}!`;
}
function GreetForm() {
const [result, action, isPending] = useActionState(submitAction, null);
return (
<form action={action}>
<input name="name" placeholder="Your name" />
<button type="submit" disabled={isPending}>
{isPending ? 'Sending...' : 'Submit'}
</button>
{result && <p>{result}</p>}
</form>
);
}Action returning an error object
Shows an action returning a structured error object that the component displays without a separate error state.
import { useActionState } from 'react';
async function loginAction(prev, formData) {
const email = formData.get('email');
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email }),
headers: { 'Content-Type': 'application/json' },
});
if (!res.ok) return { error: 'Invalid credentials.' };
return { error: null };
}
function LoginForm() {
const [state, action, pending] = useActionState(loginAction, { error: null });
return (
<form action={action}>
<input name="email" type="email" />
{state.error && <p className="error">{state.error}</p>}
<button disabled={pending}>Log in</button>
</form>
);
}Progressive enhancement with action
Replaces the form with a success message when the action returns a subscribed flag, requiring no extra useState.
import { useActionState } from 'react';
async function subscribeAction(prev, formData) {
const email = formData.get('email');
await fetch('/api/subscribe', { method: 'POST', body: new URLSearchParams({ email }) });
return { subscribed: true, email };
}
function NewsletterForm() {
const [state, action, pending] = useActionState(subscribeAction, null);
if (state?.subscribed) return <p>Subscribed as {state.email}!</p>;
return (
<form action={action}>
<input name="email" type="email" placeholder="[email protected]" />
<button disabled={pending}>{pending ? 'Subscribing...' : 'Subscribe'}</button>
</form>
);
}
Discussion