Skip to content

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

ProblemSolution
Double-submit and missing loading flagsBuilt-in isSubmitting + concurrent-submit guard
Need cancel / retry on flaky networkscancelSubmit(), retry, AbortSignal in onSubmit meta
Submit while offlineworkflow.offlineQueue

Overview

  1. submit() runs field validation
  2. On success, invokes onSubmit(values, meta)
  3. isSubmitting is true for the handler duration
  4. Concurrent submit() calls are ignored while in flight (by default)

Basics — submit handler

ts
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 / guard

Loading UI:

ts
form.state.isSubmitting;
// or form.isSubmitting()

AbortSignal and cancel

onSubmit receives meta.signal so you can abort in-flight fetches:

ts
onSubmit: async (values, meta) => {
  await fetch("/api/save", {
    method: "POST",
    body: JSON.stringify(values),
    signal: meta?.signal,
  });
},

Cancel from UI:

ts
form.cancelSubmit();

Retries

ts
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

ts
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:

ts
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.

ts
await form.submit();
await form.submit(); // → false while in flight

Opt out: form.submit({ preventDoubleSubmit: false }).


Offline queue

Queue submits while offline; flush when back online:

ts
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

ScenarioWhat to do
Retry after errorShow error UI; user clicks submit again — or use retry option
Cancel slow requestform.cancelSubmit() + honor meta.signal
OfflineEnable workflow.offlineQueue
Optimistic UIUpdate UI before onSubmit; rollback in onSubmitError

Element structure

Native HTML

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>
ts
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

tsx
<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

ts
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.

JOC is a JayOnCode monorepo of @jayoncode/* packages. Docs sync from source via TypeDoc and sync scripts.