Skip to content

Submit Button

Apply this skill when the user needs a submit button (or equivalent action element) that automatically disables itself while the form is invalid, submitting, or disabled — without manual wiring.

The hook

useSubmitButton() subscribes to the parent form’s events and returns:

{
disabled: boolean; // true while submitting OR any control is invalid OR form is disabled
isSubmitting: boolean; // true between form.submitting(true) and form.submitting(false)
disable: (b: boolean) => void; // manual override
setSubmitState: (b: boolean) => void;
isDirty: boolean; // true if any control has been changed since mount/reset
}

It must be used inside a <Form> or <NativeForm> (or any descendant of FormContext.Provider).

Pattern — Web

import { useForm, useSubmitButton } from "@mongez/react-form";
export default function SubmitButton({ children }: { children: React.ReactNode }) {
const form = useForm();
const { disabled, isSubmitting } = useSubmitButton();
return (
<button type="submit" disabled={disabled}>
{isSubmitting ? "Submitting..." : children}
</button>
);
}

With type="submit" and a click, the browser triggers the parent <form>’s submit event — no manual form?.submit() needed.

Pattern — React Native

There’s no DOM submit event in RN, so the press handler must call form.submit() explicitly:

import { useForm, useSubmitButton } from "@mongez/react-form";
import { Pressable, Text, ActivityIndicator } from "react-native";
export default function SubmitButton({ children }: { children: React.ReactNode }) {
const form = useForm();
const { disabled, isSubmitting } = useSubmitButton();
return (
<Pressable
disabled={disabled}
onPress={() => form?.submit()}
style={{ opacity: disabled ? 0.5 : 1 }}
>
{isSubmitting && <ActivityIndicator />}
<Text>{isSubmitting ? "Submitting..." : children}</Text>
</Pressable>
);
}

When disabled flips to true

  1. After form-level submitting(true) (entered the in-flight state).
  2. When the invalidControls event fires (at least one registered control failed validation).
  3. When form.disable() is called.

When disabled flips back to false

  1. After form.submitting(false) is called — typically in the .catch() or .finally() of the API request.
  2. When the validControls event fires (all controls became valid again).
  3. After form.reset().
  4. When form.disable(false) / form.enable() is called.

v4: awaited onSubmit auto-clears the submitting state

In v4, if your onSubmit returns a Promise, the engine clears the submitting state for you when it settles — on success or failure. You no longer need to call form.submitting(false) manually in that case; just return (or await) the request:

const handleSubmit = async ({ values }) => {
// engine sets submitting(true) before calling this, and submitting(false)
// automatically once this promise settles — even if it throws.
await api.createAccount(values);
navigate("/welcome");
};

How it works: the engine calls submitting(true), invokes onSubmit, and if the return value is thenable it attaches then(clear, clear) so the button re-enables whichever way the promise resolves. A synchronous (non-Promise) onSubmit keeps the v3 behavior — the engine does not auto-clear, so you must call form.submitting(false) yourself when your async work finishes (see below).

Recovering from a failed submit (sync onSubmit)

If onSubmit does not return a Promise (e.g. it kicks off a .then() chain without returning it), the engine can’t know when the work finishes — so the classic bug applies: the API request fails but the button stays disabled forever because submitting(false) was never called. Either return the promise (preferred — see above) or call submitting(false) in the failure path:

const handleSubmit = ({ values, form }) => {
// NOTE: not returning this promise, so the engine won't auto-clear.
api.createAccount(values)
.then(() => navigate("/welcome"))
.catch((err) => {
showToast(err.message);
form.submitting(false); // <-- critical: re-enables the button
});
};

Manual finally variant (also fine, but returning the promise is simpler):

const handleSubmit = async ({ values, form }) => {
try {
await api.createAccount(values);
navigate("/welcome");
} catch (err) {
showToast(err.message);
} finally {
form.submitting(false); // redundant if you let the returned promise settle
}
};

Variant — disable until dirty

By default the submit button is enabled when the form mounts (because no controls have failed validation yet). To require a change first, gate on isDirty:

const { disabled, isSubmitting, isDirty } = useSubmitButton();
const finalDisabled = disabled || !isDirty;

Useful for “Save” buttons in settings screens.

Variant — independent of useSubmitButton

If you need to react to form events directly (e.g. show a toast on validation failure), subscribe to form.on("invalidControls", ...) instead. See the form-events skill for the full event list.

Anti-patterns to avoid

  • Calling useSubmitButton outside a Form — the hook returns safe defaults (disabled: false, isSubmitting: false) but the button won’t react to form state. Always check that the button component is rendered inside a Form.
  • Storing disabled in local state and syncing manuallyuseSubmitButton already does this. Don’t double-track.
  • Forgetting form?.submit() on React Native<Pressable onPress> doesn’t auto-trigger form submission like a Web <button type="submit"> does.