Submit Lifecycle
A practical mental model for isSubmitting, isPending, isSubmitSuccessful, submitErrors, and actionResult.
1) Simple Explanation
Think of hookform-action as RHF + an action lifecycle layer.
- Idle: no request in flight.
- Submit starts:
isSubmitting = true,isPending = true,submitErrors = null. - Success:
isPending = false,isSubmitting = false,isSubmitSuccessful = true,submitErrors = null,actionResult = result. - Field error result:
isPending = false,isSubmitting = false,isSubmitSuccessful = false,submitErrors = ...,actionResult = result. - Thrown error (network/exception):
isPending = false,isSubmitting = false,isSubmitSuccessful = false. Handle this path inonError.
2) State Table
| State | What it means | When it changes | Use it for |
|---|---|---|---|
| formState.isSubmitting | Submit is running (RHF + action internal state). | True at submit start, false on finish/failure. | Legacy compatibility and debugging. |
| formState.isPending | Transition + request pending window. | Derived from useTransition plus internal pending state. | Disable button and show loading. |
| formState.isSubmitSuccessful | Last completed submit ended without field errors. | True on success, false on validation errors or thrown errors. | Post-submit success logic. |
| formState.submitErrors | Structured field-level error record. | Set on client/server validation errors, cleared at new submit start. | Render field/server validation feedback. |
| formState.actionResult | Full result object from last completed action response. | Updated on success and field-error responses. | Read confirmed payload with success guards. |
3) Correct Usage
Disable + loading with isPending
const {
handleSubmit,
formState: { isPending },
} = useActionForm(action, { defaultValues })
return (
<form onSubmit={handleSubmit()}>
<button type="submit" disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
</form>
)Post-submit success logic
const {
formState: { isPending, isSubmitSuccessful },
} = useActionForm(action, { defaultValues })
useEffect(() => {
if (!isPending && isSubmitSuccessful) {
toast.success('Saved')
// router.push('/dashboard')
}
}, [isPending, isSubmitSuccessful])Validation errors vs confirmed result
const {
formState: { submitErrors, actionResult, isSubmitSuccessful, isPending },
} = useActionForm(action, { defaultValues })
const hasFieldErrors = !!submitErrors
const confirmedData =
!isPending && isSubmitSuccessful ? actionResult : null4) Common Misinterpretations
- Using
isSubmittingfor button lock/loading. PreferisPending. - Treating
actionResultas automatic success. It can also hold an error-shaped result. - Running success side-effects only with
isSubmitSuccessful. Gate with!isPendingtoo. - Expecting
submitErrorsfor thrown exceptions. Thrown errors belong toonError.
5) Recommended Docs Snippets
Submit Button Snippet
Always show disabled and loading from isPending.
Success Effect Snippet
Use !isPending && isSubmitSuccessful to trigger post-submit effects.
Validation Errors Snippet
Show submitErrors for field-level API errors and keep RHF errors in sync.
Result Guard Snippet
Read actionResult only behind explicit success guards.
How this maps to RHF + transitions + Server Actions
RHF still owns base form mechanics (register, errors, touched/dirty state). hookform-action wraps submit execution, runs inside startTransition, then composes the final form state (`isPending = transitionPending || internalPending`). In Next.js mode, the adapter also bridges FormData/prevState signatures before passing control to the same core lifecycle.