Capítulo 14 de 39

Chapter 14: handleSubmit

Core Idea

handleSubmit(onValid, onInvalid?) returns an event handler that validates first, then calls onValid with typed data (or onInvalid with the error object) — the standard bridge between a <form onSubmit> and RHF's validation.

Key Concepts

  • Promise resolution (7.84.0+): handleSubmit(onValid)(event)'s promise resolves with whatever onValid returns, instead of discarding it.
  • Disabled inputs submit as undefined: to block editing while still submitting a value, use readOnly or disable a wrapping <fieldset> instead of the input itself.
  • Errors thrown inside onSubmit are not swallowed: wrap async requests in try/catch and call setError("root.serverError", { type, message }) in the catch to surface it — this also sets formState.isSubmitSuccessful to false.
  • Transformed resolver output needs a third generic: when a resolver (Zod .transform(), Yup, custom) changes the output shape from the field/input shape, type useForm<FormInput, unknown, FormOutput>() so handleSubmit's callback is typed as the transformed output, not the raw input.

Code Examples

const onSubmit: SubmitHandler<FormValues> = (data) => console.log(data)
const onError: SubmitErrorHandler<FormValues> = (errors) => console.log(errors)

<form onSubmit={handleSubmit(onSubmit, onError)}>
  • What it demonstrates: wiring both the success and validation-failure callbacks.
const schema = z.object({ emailOrPhone: z.string().trim() })
  .transform(({ emailOrPhone }) => ({
    isEmail: emailOrPhone.includes("@"),
    isPhone: /^\d+$/.test(emailOrPhone),
    value: emailOrPhone,
  }))

type FormInput = z.input<typeof schema>
type FormOutput = z.output<typeof schema>

const { register, handleSubmit } = useForm<FormInput, unknown, FormOutput>({
  resolver: zodResolver(schema),
})
const onSubmit: SubmitHandler<FormOutput> = (data) => { /* data.isEmail, data.isPhone */ }
  • What it demonstrates: typing handleSubmit's callback correctly when a resolver's .transform() changes the output shape.

Anti-patterns

  • Letting an async onSubmit throw without a try/catch: handleSubmit doesn't swallow it, but nothing surfaces it to the user either unless you catch it and call setError.
  • Disabling an input to "lock" its value while keeping it in submission data: disabled inputs submit as undefined — use readOnly or a disabled <fieldset> instead.
  • Omitting the third TTransformedValues generic when using a transforming resolver: the callback gets typed as the raw input shape, producing type errors on the transformed properties.

Key Takeaways

  1. handleSubmit always validates before calling either callback — it's the single entry point for a valid/invalid submit branch.
  2. Server-side submission errors belong in a try/catch around your async call, surfaced via setError("root.serverError", ...).
  3. A transforming resolver needs useForm<Input, Context, Output>()'s third generic for handleSubmit's callback to type-check against the transformed shape.

Connects To

  • useform-seterror: how to surface a caught submission error.
  • useform: the resolver option and its TTransformedValues generic.
  • useform-form: the higher-level <Form> component built around the same validate-then-submit flow.