<form>

Submits a form by running a JavaScript function instead of navigating to a URL.

Signature
<form action={search}>
  <input name="query" />
  <button type="submit">Search</button>
</form>
  • Added in React 19.0.0

Parameters

ParameterTypeDescription
$actionoptionalstring | ((formData: FormData) => void | Promise<void>)A URL or a function. A URL makes the form behave like a plain HTML form. A function handles the submission in a Transition and is called with a single FormData argument; it may be async. Can be overridden per button by formAction on a <button>, <input type="submit"> or <input type="image">.
$childrenoptionalReactNodeThe form controls. Only fields with a name prop appear in the FormData passed to a function action.
$...propsoptionalHTMLFormElement props<form> supports all common element props, including method, encType, target, noValidate, onSubmit, ref, className and style.

Return value

ReactElementRenders a browser <form> element. When action is a function, React collects the fields into a FormData object, calls the function with it inside a Transition, and resets the form if the call finishes without throwing.

Errors

An error thrown inside a function action is not caught for you — it propagates to the nearest error boundary.

Changelog

  • PHP 19.0.0action and formAction accept functions, which React calls with the form's FormData inside a Transition.
  • PHP 19.0.0React resets the form after a function action resolves without throwing, so uncontrolled fields return to their defaultValue.

A <form> whose action is a function stops behaving like a navigation. React gathers the named fields into a FormData object, calls your function with it, and runs the call inside a Transition — so the submission is tracked, and any component rendered below the form can read that pending state with useFormStatus.

When to reach for it

Use a function action whenever submitting does work rather than moving the user to a new page: saving a draft, adding a row, calling a mutation. It collapses the usual trio of onSubmit, event.preventDefault() and a hand-rolled isSubmitting flag into one prop.

What trips people up

  • Your function receives FormData, not the submit event. There is nothing to prevent, and a field without a name is simply absent.
  • After the action resolves without throwing, React resets the form. Uncontrolled fields snap back to their defaultValue, which reads as data loss if you did not expect it.
  • When the action is a function the request uses POST, whatever the method prop says.
  • A formAction on a submit button wins over the form's own action, which is how one form runs two different operations.

Example

Try it yourself
Loading editor…
Loading React…

When to use it

  • Save a settings panel without a page navigation or a manual preventDefault call
  • Give one form two submit buttons — Save and Delete — via formAction overrides
  • Expose a pending state to a shared submit button through useFormStatus

Frequently asked questions

Why is my form clearing itself after submit?

React resets a form after a function action completes without throwing, so uncontrolled fields return to their defaultValue. If you need the values to persist, hold them in state as controlled inputs, or return the submitted values from the action with useActionState and feed them back as defaults.

How do I get the submit event inside a form action?

You cannot — a function action is called with FormData, not with an event. If you specifically need the event (to call stopPropagation, or to inspect event.nativeEvent.submitter), use onSubmit with preventDefault instead, or read the intent from a formAction on the button that was clicked.

Why does my form send POST when I set method='get'?

Passing a function to action or formAction forces the HTTP method to POST regardless of the method prop. Only a string URL action honours method='get'. If you want a GET navigation with query parameters, pass a URL string rather than a function.

More examples

Two actions in one form with formAction

Both buttons submit the same form, but the second one carries its own formAction. Clicking Save prints `saved: Untitled`; clicking Discard prints `discarded: Untitled`. The FormData handed to each function is identical — only the function differs. The form's own action is used for any submit control that does not override it, including pressing Enter in the text field.

Example · jsx
import { useState } from "react";

export default function DraftForm() {
  const [log, setLog] = useState("idle");

  function save(formData) {
    setLog(`saved: ${formData.get("title")}`);
  }

  function discard(formData) {
    setLog(`discarded: ${formData.get("title")}`);
  }

  return (
    <form action={save}>
      <label>
        Title: <input name="title" defaultValue="Untitled" />
      </label>
      <button type="submit">Save</button>
      <button type="submit" formAction={discard}>
        Discard
      </button>
      <p>{log}</p>
    </form>
  );
}

Returning errors from an action with useActionState

useActionState wraps the action so it can return a value that survives the submission. Submitting with the default `ada` renders `Error: "ada" is taken.`; typing an unused name renders `Claimed <name>`. Because the action returns an error object rather than throwing, no error boundary is involved and the form stays on screen — this is the standard way to surface validation failures from a form action.

Example · jsx
import { useActionState } from "react";

const KNOWN = ["ada", "grace", "alan"];

async function claimName(prevState, formData) {
  const name = String(formData.get("name") || "").toLowerCase();
  await Promise.resolve();
  if (!name) return { error: "Name is required.", value: null };
  if (KNOWN.includes(name)) return { error: `"${name}" is taken.`, value: null };
  return { error: null, value: name };
}

export default function ClaimNameForm() {
  const [state, formAction, isPending] = useActionState(claimName, {
    error: null,
    value: null,
  });

  return (
    <form action={formAction}>
      <label>
        Username: <input name="name" defaultValue="ada" />
      </label>
      <button type="submit" disabled={isPending}>Claim</button>
      {state.error && <p>Error: {state.error}</p>}
      {state.value && <p>Claimed {state.value}</p>}
    </form>
  );
}

A URL action still works as plain HTML

With a string action, React does not intercept anything: the browser navigates to /search?query=react+forms and the method prop is honoured. This is the escape hatch when you want a real navigation, a GET with query parameters in the URL, or a form that works before any JavaScript has loaded. Swap the string for a function and the same markup becomes a client-side submission that always uses POST.

Example · jsx
export default function SearchForm() {
  return (
    <form action="/search" method="get">
      <label>
        Query: <input name="query" defaultValue="react forms" />
      </label>
      <button type="submit">Search</button>
    </form>
  );
}

Discussion

  • Be the first to comment on this lesson.