Submission
Send valid data to your API — with loading state, cancel, retries, and offline queueing.
Previous: Validation · Next: State
Playground
Submission explorer → — flaky API, offline queue, double-submit guard, cancel.
Problem → solution
| Problem | Solution |
|---|---|
| Double-submit and missing loading flags | Built-in isSubmitting + concurrent-submit guard |
| Need cancel / retry on flaky networks | cancelSubmit(), retry, AbortSignal in onSubmit meta |
| Submit while offline | workflow.offlineQueue |
Overview
submit()runs field validation- On success, invokes
onSubmit(values, meta) isSubmittingistruefor the handler duration- Concurrent
submit()calls are ignored while in flight (by default)
Basics — submit handler
const form = createForm({
initialValues: { message: "" },
validators: { message: [required] },
onSubmit: async (values, meta) => {
await api.sendMessage(values, { signal: meta?.signal });
},
});
await form.submit(); // true on success, false on validation fail / cancel / guardLoading UI:
form.state.isSubmitting;
// or form.isSubmitting()AbortSignal and cancel
onSubmit receives meta.signal so you can abort in-flight fetches:
onSubmit: async (values, meta) => {
await fetch("/api/save", {
method: "POST",
body: JSON.stringify(values),
signal: meta?.signal,
});
},Cancel from UI:
form.cancelSubmit();Retries
await form.submit({
retry: 3, // number of attempts
});
await form.submit({
retry: {
maxAttempts: 3,
delayMs: 250,
// optional shouldRetry(error, attempt)
},
});Include object diff in meta
await form.submit({ includeDiff: true });
onSubmit: async (values, meta) => {
console.log(meta?.diff); // FormDiffResult when includeDiff is set
},Requires optional peer @jayoncode/object-diff.
Error handling
If onSubmit throws, submit() returns false and onSubmitError runs:
createForm({
onSubmit: async (values) => {
const res = await fetch("/api/signup", {
method: "POST",
body: JSON.stringify(values),
});
if (!res.ok) throw new Error("Signup failed");
},
onSubmitError: (error) => {
toast.error(error instanceof Error ? error.message : "Something went wrong");
},
});Double-submit guard
Calling submit() again while the first is still running returns false — your handler is not called twice.
await form.submit();
await form.submit(); // → false while in flightOpt out: form.submit({ preventDoubleSubmit: false }).
Offline queue
Queue submits while offline; flush when back online:
createForm({
initialValues: { email: "" },
validators: { email: [required, email] },
workflow: {
offlineQueue: {
enabled: true,
storageKey: "signup:offline",
},
},
onSubmit: async (values) => api.register(values),
});
form.state.submissionQueue.pending;
form.state.submissionQueue.flushing;
await form.flushOfflineQueue();Pair with createBrowserLifecyclePlugin({ flushOfflineQueueOnOnline: true }) — see Integrations.
Common patterns
| Scenario | What to do |
|---|---|
| Retry after error | Show error UI; user clicks submit again — or use retry option |
| Cancel slow request | form.cancelSubmit() + honor meta.signal |
| Offline | Enable workflow.offlineQueue |
| Optimistic UI | Update UI before onSubmit; rollback in onSubmitError |
Element structure
Native HTML
<form id="contact">
<label>
Message
<textarea name="message" rows="4"></textarea>
</label>
<button type="submit">Send</button>
<button type="button" id="cancel">Cancel</button>
</form>createForm({
target: "#contact",
schema: { message: { required: true } },
onSubmit: async (values, meta) => {
await api.send(values, { signal: meta?.signal });
},
});
document.querySelector("#cancel")?.addEventListener("click", () => {
form.cancelSubmit();
});While submitting, the engine sets aria-busy on the form and disables submit buttons.
React JSX
<form {...form.form()}>
<label>
Message
<textarea {...form.field("message")} rows={4} />
{form.state.errors.message ? <span role="alert">{form.state.errors.message}</span> : null}
</label>
<button {...form.submit()} disabled={form.state.isSubmitting || form.state.ui.submitDisabled}>
{form.state.isSubmitting ? "Sending…" : "Send"}
</button>
<button type="button" onClick={() => form.cancelSubmit()} disabled={!form.state.isSubmitting}>
Cancel
</button>
</form>Cheat sheet
const ok = await form.submit({ retry: 2, includeDiff: true });
form.cancelSubmit();
form.state.isSubmitting;
form.state.submitCount;
form.state.submissionQueue;
await form.flushOfflineQueue();Next: State — snapshots, field meta, undo/redo, diffs.
