MENU
useActionState
useActionState() is a Hook, introduced in React 19, for wrapping an action function so React tracks its pending status and latest returned state for you. It takes an action function and an initial state, and returns [state, formAction, isPending]: state starts out as the initial state and becomes whatever the action last returned; formAction is a wrapped version of the action, meant to be passed straight to a <form>'s action prop; and isPending is a boolean that's true while that action is in flight.
The action function itself receives (previousState, formData) and returns the new state — similar to a useReducer() reducer, except it's explicitly allowed to be asynchronous and to perform side effects like network requests, which a useReducer() reducer must never do. When used as a <form>'s action, the second argument is the form's submitted FormData automatically; React also wraps the call in a transition for you, and resets any uncontrolled fields in the form once the action completes successfully.
An optional third argument, permalink, names a fallback URL for progressive enhancement: with a server-rendered form, if the action is triggered before the page's JavaScript has finished loading, the browser navigates there instead. It has no effect once the page is already interactive, which is always the case in the client-only demos on this site.
Earlier React 19 canary builds called this Hook useFormState(); it was renamed to useActionState() before the stable release to make clear it works with any action, not just ones attached to a form. If you run into useFormState() in an older blog post or tutorial, it's this same Hook under its previous name.
Unlike the use() Hook covered previously, useActionState() follows the normal Rules of Hooks: it must be called unconditionally, at the top level of the component. If formAction is triggered again while a previous call is still pending, React queues the calls and runs them one at a time, passing each one the previous call's returned state.
RESETRUNFULL
<!DOCTYPE html><html><head>
<script type="importmap">
{"imports": {"react": "https://esm.sh/react@19", "react-dom/client": "https://esm.sh/react-dom@19/client"}}
</script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head><body>
<div id="root"></div>
<script type="text/babel" data-type="module" data-presets="react">
import { useActionState } from 'react';
import { createRoot } from 'react-dom/client';
async function updateName(previousState, formData) {
const newName = formData.get('name');
await new Promise((resolve) => setTimeout(resolve, 1000)); // simulate a slow server
if (!newName || newName.trim() === '') {
return { ...previousState, error: 'Name cannot be empty.' };
}
return { name: newName, error: null };
}
function App() {
const [state, formAction, isPending] = useActionState(updateName, { name: 'Guest', error: null });
return (
<form action={formAction}>
<p>Current name: {state.name}</p>
{state.error && <p>Error: {state.error}</p>}
<input type="text" name="name" disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
</form>
);
}
createRoot(document.getElementById('root')).render(<App />);
</script>
</body></html>