Modern React 14: Forms with Actions
This is part 14 of the Modern React development series.
React 19 treats forms as first-class mutation surfaces. A form can call an Action, expose pending state, return result state, and support progressive enhancement when the framework integrates server features.
Concept
An Action is a function invoked by a form or a Transition. useActionState connects an Action to returned state and pending state. useFormStatus lets a nested submit component read the pending state of its parent form.
Terms
- Action: A function React treats as an ordered unit of work from a form or Transition.
- FormData: The browser object that carries submitted form field values.
- Pending state: Whether a form or Action submission is still running.
- Progressive enhancement: A path where a form can still submit before the client JavaScript is fully ready, when the framework supports it.
Mental model
Think of the form as a conveyor belt. The browser gathers fields into FormData, the Action processes them, and React brings back a result plus pending status for the UI.
How it is used
Use form Actions for profile edits, settings forms, contact forms, checkout steps, and server-backed mutations where the submit event is the natural boundary for collecting input and returning validation or success state.
How to use it
- Write an Action that accepts previous state and
FormDatawhen usinguseActionState. - Return a serializable state object with success, field errors, or a message.
- Pass the returned form action to the form’s
actionprop. - Render pending feedback with the third value from
useActionStateoruseFormStatusin a child component. - Keep field names stable because
FormDatareads by name.
Example: Profile form state
import { useActionState } from "react";import { updateProfile } from "./profileApi";
type ProfileState = { message: string;};
async function saveProfile( previousState: ProfileState, formData: FormData,): Promise<ProfileState> { const displayName = String(formData.get("displayName") ?? "").trim();
if (displayName.length < 2) { return { message: "Display name needs at least two characters." }; }
await updateProfile({ displayName }); return { message: "Profile saved." };}
export function ProfileForm() { const [state, formAction, isPending] = useActionState(saveProfile, { message: "", });
return ( <form action={formAction}> <label> Display name <input name="displayName" /> </label> <button disabled={isPending}>Save</button> <p>{state.message}</p> </form> );}The Action receives form data, returns display state, and lets the button reflect pending status.
Example: Nested submit button with useFormStatus
import { useFormStatus } from "react-dom";
function SubmitButton() { const { pending } = useFormStatus(); return <button disabled={pending}>{pending ? "Saving..." : "Save"}</button>;}
export function SettingsForm({ action }: { action: (data: FormData) => void }) { return ( <form action={action}> <input name="timezone" /> <SubmitButton /> </form> );}useFormStatus reads the parent form, so the submit button can stay reusable without receiving pending props.
Details to watch
- Form names: Every submitted field needs a
nameforFormDatato include it. - Hook placement:
useFormStatusreads a parent form, not a form returned by the same component. - Serializability: Server-backed Actions need serializable input and output values.
- Side effects:
useActionStatereducer actions may be async and perform side effects, unlikeuseReducerreducers.
Series navigation
- Previous: Part 13: Transitions for responsive updates
- Next: Part 15: Optimistic UI
- Series index: Modern React development