# handleSubmit

> Validate form inputs and invoke your submit callback with typed form data, or pass errors to an error handler on validation failure.

Source: https://react-hook-form.com/docs/useform/handlesubmit

## `handleSubmit:` [UseFormHandleSubmit](https://react-hook-form.com/ts#UseFormHandleSubmit)

This function will receive the form data if form validation is successful.

### Props

---

| Name               | Type                                                                                                | Description            |
| ------------------ | --------------------------------------------------------------------------------------------------- | ---------------------- |
| SubmitHandler      | `(data: TFieldValues, e?: BaseSyntheticEvent) => TResult \| Promise<TResult>`  | A successful callback. |
| SubmitErrorHandler | `(errors: FieldErrors, e?: BaseSyntheticEvent) => unknown \| Promise<unknown>` | An error callback.     |

> **Important: Rules**
>
> - _(since v7.84.0)_ The promise returned by calling
>   `handleSubmit(onValid)(event)` resolves with whatever `onValid` returns,
>   instead of discarding it.
>
>   ```javascript
>   const onValid = (data) => ({ id: data.id })
>   const result = await handleSubmit(onValid)() // result: { id: string } | undefined
>   ```
>
> - `formState.isSubmitting` stays `true` until the promise returned by `onSubmit` settles, so `await` (or return) your async work:
>
>   ```javascript
>   const onSubmit = async (data) => {
>     await fetch("/api", { method: "POST", body: JSON.stringify(data) }) // ✅ isSubmitting stays true until this resolves
>   }
>   ```
>
> - `disabled` inputs will appear as `undefined` values in form values. If you want to prevent users from updating an input and wish to retain the form value, you can use `readOnly` or disable the entire &lt;fieldset /&gt;. Here is an [example](https://codesandbox.io/s/react-hook-form-disabled-inputs-oihxx).
> - `handleSubmit` function will not swallow errors that occurred inside your onSubmit callback, so we recommend that you use try/catch blocks inside your async requests and handle those errors gracefully for your customers. Use `setError` inside the catch block to register a server-side error — this also ensures `formState.isSubmitSuccessful` is set to `false`.
>
>   ```javascript
>   const onSubmit = async (data) => {
>     // async request which may result error
>     try {
>       // await fetch()
>     } catch (e) {
>       setError("root.serverError", {
>         type: e.status,
>         message: e.message,
>       })
>     }
>   }
> 
>   return <form onSubmit={handleSubmit(onSubmit)} />
>   ```
>
> - To submit from a button outside the `<form>`, either link the button to the form with the native `form` attribute, or call the returned function directly:
>
>   ```javascript
>   <form id="profile-form" onSubmit={handleSubmit(onSubmit)}>...</form>
> 
>   <button type="submit" form="profile-form">Save</button> // ✅ native form attribute
>   <button type="button" onClick={handleSubmit(onSubmit)}>Save</button> // ✅ call handleSubmit directly
>   ```
>
> - React's `submit` event bubbles through the component tree, including portals. If a form is rendered inside another form (for example in a modal), stop propagation so the outer form doesn't submit too:
>
>   ```javascript
>   <form
>     onSubmit={(event) => {
>       event.stopPropagation()
>       handleSubmit(onInnerSubmit)(event)
>     }}
>   />
>   ```
>
> - For submitting with React Server Actions or `useActionState`, see [Server Actions](https://react-hook-form.com/advanced-usage#ServerActionsuseActionState). To test your submit flow, see [Testing Form](https://react-hook-form.com/advanced-usage#TestingForm).
>

### Examples

---

**Sync**

**TS**

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-handlesubmit-ts-v7-lcrtu)

```tsx
import { useForm, SubmitHandler, SubmitErrorHandler } from "react-hook-form"

type FormValues = {
  firstName: string
  lastName: string
  email: string
}

export default function App() {
  const { register, handleSubmit } = useForm<FormValues>()
  const onSubmit: SubmitHandler<FormValues> = (data) => console.log(data)
  const onError: SubmitErrorHandler<FormValues> = (errors) =>
    console.log(errors)

  return (
    <form onSubmit={handleSubmit(onSubmit, onError)}>
      <input {...register("firstName")} />
      <input {...register("lastName")} />
      <input type="email" {...register("email")} />

      <input type="submit" />
    </form>
  )
}
```

**JS**

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-handlesubmit-v7-uqmiy)

```javascript
import { useForm } from "react-hook-form"

export default function App() {
  const { register, handleSubmit } = useForm()
  const onSubmit = (data, e) => console.log(data, e)
  const onError = (errors, e) => console.log(errors, e)

  return (
    <form onSubmit={handleSubmit(onSubmit, onError)}>
      <input {...register("firstName")} />
      <input {...register("lastName")} />
      <button type="submit">Submit</button>
    </form>
  )
}
```

**Async**

[Open in CodeSandbox](https://codesandbox.io/s/react-hook-form-async-submit-validation-kpx0o)

```javascript
import { useForm } from "react-hook-form"

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

function App() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm()
  const onSubmit = async (data) => {
    await sleep(2000)
    if (data.username === "bill") {
      alert(JSON.stringify(data))
    } else {
      alert("There is an error")
    }
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label htmlFor="username">User Name</label>
      <input placeholder="Bill" {...register("username")} />

      <input type="submit" />
    </form>
  )
}
```

**Resolver with transformed values**

When a resolver (Zod, Yup, Joi, or custom) transforms the submitted data — so the output type differs from the field input type — use `useForm`'s third generic, `TTransformedValues`, to tell TypeScript what `handleSubmit` will receive. Without it, the callback is typed as the input (field) type and you'll get errors accessing transformed properties.

```tsx
import { useForm, SubmitHandler } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"

// Example using Zod — the same pattern applies to any resolver whose
// output type differs from its input type (Yup, Joi, custom, etc.)
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> // { emailOrPhone: string }
type FormOutput = z.output<typeof schema> // { isEmail: boolean; isPhone: boolean; value: string }

export default function App() {
  // 1st generic = field/input type (drives register, watch, etc.)
  // 3rd generic = transformed output type (drives handleSubmit callback)
  const { register, handleSubmit } = useForm<FormInput, unknown, FormOutput>({
    resolver: zodResolver(schema),
  })

  const onSubmit: SubmitHandler<FormOutput> = (data) => {
    console.log(data.isEmail, data.isPhone, data.value)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("emailOrPhone")} />
      <input type="submit" />
    </form>
  )
}
```

---

### Video

---

The following video tutorial explains the `handleSubmit` API in detail.

[Watch on YouTube](https://www.youtube.com/watch?v=KzcPKB9SOEk)

### Tips

---

- `handleSubmit` does not block a second submission while the first one is still pending. Disable the submit button with `formState.isSubmitting`, or check it at the start of your handler.
- Use the second argument, `onInvalid`, to react to failed validation, for example to log analytics or open the section that contains the first error:

  ```javascript
  handleSubmit(onValid, (errors) => {
    analytics.track("form_invalid", { fields: Object.keys(errors) })
  })
  ```

- Transform the data inside `onValid` (or in your resolver) and not in the inputs. Inputs keep their raw values, and you send the shape your API needs.
- When you call `handleSubmit(onValid)()` without an event, nothing calls `preventDefault`. Use this to submit from a keyboard shortcut or a timer.
