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.

  1. Idle: no request in flight.
  2. Submit starts: isSubmitting = true, isPending = true, submitErrors = null.
  3. Success: isPending = false, isSubmitting = false, isSubmitSuccessful = true, submitErrors = null, actionResult = result.
  4. Field error result: isPending = false, isSubmitting = false, isSubmitSuccessful = false, submitErrors = ..., actionResult = result.
  5. Thrown error (network/exception): isPending = false, isSubmitting = false, isSubmitSuccessful = false. Handle this path in onError.

2) State Table

StateWhat it meansWhen it changesUse it for
formState.isSubmittingSubmit is running (RHF + action internal state).True at submit start, false on finish/failure.Legacy compatibility and debugging.
formState.isPendingTransition + request pending window.Derived from useTransition plus internal pending state.Disable button and show loading.
formState.isSubmitSuccessfulLast completed submit ended without field errors.True on success, false on validation errors or thrown errors.Post-submit success logic.
formState.submitErrorsStructured field-level error record.Set on client/server validation errors, cleared at new submit start.Render field/server validation feedback.
formState.actionResultFull 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 : null

4) Common Misinterpretations

  • Using isSubmitting for button lock/loading. Prefer isPending.
  • Treating actionResult as automatic success. It can also hold an error-shaped result.
  • Running success side-effects only with isSubmitSuccessful. Gate with !isPending too.
  • Expecting submitErrors for thrown exceptions. Thrown errors belong to onError.

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.