submitly

Introduction

submitly is the headless submission layer for React and Next.js forms, dialogs and mutations.

submitly is a headless form submission layer for React and Next.js. It sits on top of react-hook-form and adds everything that happens between "the user pressed Save" and "the screen is up to date again":

  • Standard Schema validation on the client (Zod 3.24+ / 4, Valibot 1, ArkType 2, ...).
  • A typed action that is any (input) => Promise<result>: a server action, a fetch wrapper, an RPC call.
  • Server errors mapped to fields. A failed result carries fieldErrors keyed by path, applied with form.setError, plus form-level errors exposed as serverErrors.
  • Pending and dialog locks. Double submits are ignored, and the surrounding dialog cannot be closed (escape, overlay click, close button) while a save is in flight.
  • Post-mutation conventions. Toast, refresh, reset the form, redirect: wired once in a provider, overridable per call.
  • A server half for Next.js. runAction re-validates input with the same schema, revalidates paths and tags on success, and turns thrown errors into a failed result instead of a crashed request.

It renders nothing and imports no toast library, router or i18n library, so it works with plain elements, shadcn, Radix, Base UI, Headless UI, or a native <dialog>.

What it is not

  • Not a form library. Field state, registration and rendering stay with react-hook-form.
  • Not a UI kit. There are no inputs, dialogs, buttons or error panels. Bring your own components.
  • Not an API client or auth layer. Transport, tokens and retries are yours. An action is just a function.
  • Not a state-management or caching layer. Use TanStack Query, SWR or the Next.js cache for that; submitly only tells them when a mutation succeeded.
  • Not localized. The package has no visible strings of its own. Your components and your runAction messages carry the user-facing copy in whatever language you ship.

submitly is pre-release (0.x). The API can still change before 1.0. TypeScript 5.4 or newer is required for consumers.

How it compares

Versus hand-rolled react-hook-form and try/catch

Every app that submits forms with react-hook-form ends up writing the same 40 lines in each form. submitly is those 40 lines, written once and tested.

ConcernHand-rolled RHF + try/catchsubmitly
Schema resolverPick and wire a resolver per schema libraryAny Standard Schema, no resolver to choose
Submit handlerhandleSubmit + try / catch / finally in every formuseCrudForm({ schema, action })
Thrown errors (network, 500)Easy to forget; an unhandled rejection or a stuck spinnermapError, set once; the action cannot make the returned promise reject
Server field errorsLoop over the response and call setError yourselffieldErrors keyed by dotted path are applied for you, first field focused
Form-level server errorsYour own useStateserverErrors
Double submitA ref or disabled flag, per formBuilt in for forms and for useCrudAction
Toast on resulttoast.* calls scattered in every handlerOne notify in the provider, per-call override
Refresh after successrouter.refresh() / invalidateQueries() per handleronMutated in the provider
Reset after successform.reset() by handresetOnSuccess (default on)
Redirect after successrouter.push() by handredirectTo through the provider's navigate
Dialog closes mid-saveManual guard around every close pathAutomatic dialog lock
Form keeps stale values after reopening a dialogManual key bookkeepinguseControllableDialog().openKey
Non-form mutation (delete, toggle)Another bespoke hookuseCrudAction with the same conventions
Server action validationParse input, catch, revalidatePath, map errorsrunAction

Versus next-safe-action

next-safe-action and similar libraries (zsa, tRPC) define how a server action is built and called: middleware, typed inputs and outputs, server errors. submitly defines what the form and the UI do with the result. They are complementary, not competing: their action, our hooks.

Concernnext-safe-actionsubmitly
Typed server action with middleware and contextYesNo (runAction is a thin validate-and-revalidate wrapper)
Works outside Next.jsNoYes (submitly is plain React)
Form state on top of react-hook-formThrough its hooks and an optional adapteruseCrudForm
Dialog lock, form remount on openNoYes
Non-form mutation with notify, refresh and redirect conventionsPer-hook callbacksuseCrudAction, conventions set once in a provider
One provider for toast, refresh and navigationNoYes

If you already use next-safe-action, keep it. Pass the safe action to useCrudForm as the action and teach submitly its result shape with normalize. Custom API shape shows how.

Where to go next

Edit on GitHub

On this page