Submission Handling

function LoginForm() {
  const form = useForm({
    initialValue: { email: "", password: "" },
    onSubmit: async (form) => {
      await login(form.value);
    },
    onSubmitError: (form, error) => {
      toast.error("Failed to log in");
    },
  });
 
  return <form onSubmit={form.handleSubmit}>{/* ... */}</form>;
}

handleSubmit:

  1. Waits out any pending validation.
  2. If the form is invalid, marks it touched (so errors on never-blurred fields become visible) and calls onSubmitInvalid, then returns.
  3. Otherwise sets submitting to true, calls onSubmit, and falls back to onSubmitError if it throws/rejects.

A no-op on a re-entrant call while already submitting. The event parameter is optional and used only for preventDefault(), so the same call works from a React Native onPress, any caller with no event, or a web <form onSubmit> as shown above.

Disabling the submit button while submitting

submitting (and dirty, for a "nothing to save" state) are ordinary reactive state, so gate the button like any other field property:

<Watch api={form} select={(f) => [f.submitting, f.dirty] as const}>
  {(form, [submitting, dirty]) => (
    <button type="submit" disabled={submitting || !dirty}>
      Save
    </button>
  )}
</Watch>;

Disabling the whole form while submitting

Set form.disabled = true around onSubmit's work. disabled cascades from a field down through every already-registered descendant, so form.disabled = true reaches every field in the tree without watching submitting anywhere:

const form = useForm({
  initialValue: { email: "", password: "" },
  onSubmit: async (form) => {
    form.disabled = true;
    try {
      await login(form.value);
    } finally {
      form.disabled = false;
    }
  },
});

disabled on its own only skips validation; it doesn't reach the DOM by itself. For it to actually disable an input, the component rendering that input has to read its own field's disabled and fold it into whatever disabled prop the caller passed in, the same way TextField does (see Basic):

const isDisabled = disabled || field.disabled;

Each TextField is already subscribed to just its own field via useWatch, so disabling a 50-field form during submit re-renders only the fields whose disabled actually flipped, not LoginForm itself.

What's next

  • FormApi — full reference on JSR