Forms and Actions

This section documents React 19.x for the web, using function components and Hooks throughout (class components appear only where React still requires them, such as error boundaries). React Native is out of scope and gets only a pointer. The React 19 additions follow the current official React documentation. This content was generated with the assistance of AI and should be verified against react.dev before being relied on in production, since React APIs continue to evolve between releases.

This section’s bibliography lists the reference material consulted while preparing these pages.

React handles forms in two styles: the classic controlled inputs + onSubmit approach, and the React 19 Actions model, which wires an async function directly to a <form> and manages pending, error, and optimistic state for you.

Controlled inputs

Bind each field’s value to state and update it in onChange. The component is the single source of truth.

function SignupForm({ onSubmit }) {
  const [email, setEmail] = useState('');
  const [note, setNote] = useState('');
  const [plan, setPlan] = useState('free');

  function handleSubmit(e) {
    e.preventDefault();
    onSubmit({ email, plan });
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
      <textarea value={note} onChange={(e) => setNote(e.target.value)} />
      <select value={plan} onChange={(e) => setPlan(e.target.value)}>
        <option value="free">Free</option>
        <option value="pro">Pro</option>
      </select>
      <button type="submit">Create account</button>
    </form>
  );
}

For a form with many fields you do not need to react to per keystroke, leave them uncontrolled and read new FormData(e.target) on submit. See <form>.

React 19 Actions

Pass a function to <form action> (or to startTransition). React calls it with the form’s FormData, marks the surrounding transition pending while it runs, resets uncontrolled fields on success, and surfaces thrown errors.

import { useActionState } from 'react';

function CommentForm({ postId }) {
  const [state, formAction, isPending] = useActionState(
    async (prevState, formData) => {
      const text = formData.get('text');
      try {
        await addComment(postId, text);
        return { ok: true, error: null };
      } catch (err) {
        return { ok: false, error: err.message };
      }
    },
    { ok: false, error: null }
  );

  return (
    <form action={formAction}>
      <textarea name="text" required />
      <button disabled={isPending}>{isPending ? 'Posting…' : 'Post'}</button>
      {state.error && <p role="alert">{state.error}</p>}
    </form>
  );
}
  • useActionState(fn, initialState)[state, action, isPending]. state is the last value the action returned.

  • useFormStatus() (from react-dom) — lets a child of the <form> (e.g. a <SubmitButton>) read { pending } without prop drilling.

  • useOptimistic(value, updateFn) — render an immediate optimistic value while the action is in flight, automatically reverting if it fails.

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? 'Saving…' : 'Save'}</button>;
}

When the action is a server function, <form action> also works before JavaScript loads (progressive enhancement) — the browser posts the form and the server responds. References: useActionState, useFormStatus, useOptimistic, Server Functions.