useActionState

useActionState(action, initialState) wraps an action so React 7,897 tracks its result and its pending flag for you, returning [state, formAction, isPending]. Your action is called with (previousState, formData) and returns the next state — a reducer, except that this one may be async and may perform side effects. Pass formAction to the form's action prop; state is whatever the last call returned, which makes it the natural home for a validation error or a success message.

Threading a result and an error through useActionStateHTML
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@8.0.5/babel.min.js"></script>
<script type="importmap">{ "imports": { "react": "https://esm.sh/react@19.3.0",
  "react-dom/client": "https://esm.sh/react-dom@19.3.0/client" } }</script>
<script type="text/babel" data-type="module">
import React, { useActionState } from 'react';
import { createRoot } from 'react-dom/client';
async function updateName(previous, formData) {
  const name = formData.get('name').trim();
  await new Promise(r => setTimeout(r, 400));            // simulate a slow server
  if (!name) return { ...previous, error: 'Name cannot be empty.' };
  return { name, error: null };
}
const initial = { name: 'Guest', error: null };
function App() {
  const [state, formAction, isPending] = useActionState(updateName, initial);
  return (
    <form action={formAction} style={{ font: '15px system-ui' }}>
      <p>Current name: <b>{state.name}</b></p>
      {state.error && <p style={{ color: 'crimson' }}>{state.error}</p>}
      <input name="name" disabled={isPending} />
      <button disabled={isPending}>{isPending ? 'Saving...' : 'Save'}</button>
    </form>
  );
}
createRoot(document.getElementById('root')).render(<App />);
setTimeout(() => { document.querySelector('input').value = 'Ada';
  document.querySelector('button').click(); }, 300);
</script>
Browser output of Listing 2.70
Browser output of 70

Returning an error object instead of throwing is the pattern to learn: a thrown error reaches the nearest error boundary and takes the form down with it, while a returned { error } renders next to the field. Submit again while a call is in flight and React queues the calls, passing each one the previous result. React 19 canaries named this Hook useFormState and exported it from react-dom; that is the same Hook under its old name.