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
- After form-level
submitting(true)(entered the in-flight state). - When the
invalidControlsevent fires (at least one registered control failed validation). - When
form.disable()is called.
When disabled flips back to false
- After
form.submitting(false)is called — typically in the.catch()or.finally()of the API request. - When the
validControlsevent fires (all controls became valid again). - After
form.reset(). - 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
useSubmitButtonoutside 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
disabledin local state and syncing manually —useSubmitButtonalready 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.