Form Component
Submit validated React forms to an API, Server Action, or callback with built-in loading, error, and success state management.
</> Form: FormProps Since v7.44.0
Note: This component is currently in BETA
React Hook Form's Form component is an optional form submission component. It renders a native <form> element, runs your validation through handleSubmit, and then sends the data to an API, a Server Action, or your callback. Loading, success, and error state end up in formState.
By default, it sends a POST request with your form submission data as FormData. Set encType (or a Content-Type header via the headers prop) to application/json to send JSON instead.
- Progressive enhancement for your form.
- Supports both React Web and React Native.
- Handles submission, including server errors.
<Formaction="/api"method="post" // default to postonSubmit={() => {}} // function to be called before the requestonSuccess={() => {}} // valid responseonError={() => {}} // error responsevalidateStatus={(status) => status >= 200 && status < 300} // the default/>
How it works
When the user submits, Form goes through these steps:
- Validate. The form is validated the same way as
handleSubmit. If validation fails, the request is not sent, andonSubmit,onSuccess, andonErrorare not called. Field errors appear informState.errorsas usual. onSubmit. YouronSubmitcallback is called with the validateddataand the same values encoded asformDataandformDataJson. If it returns a promise,Formwaits for it.- Send the request.
- If
actionis a URL string,Formcallsfetch(action, { method, headers, body }). Then it checks the response status withvalidateStatus. It callsonSuccess({ response })when the status is valid, andonError({ response })when it is not. - If
actionis a function,Formcalls it with theFormData. If the function throws,onError({ error })is called. - If there is no
action, nothing is sent. OnlyonSubmitruns, so you can send the request yourself.
- If
- Update form state.
formState.isSubmittingstaystrueuntil all of the above finishes. If the request fails,formState.isSubmitSuccessfulis set tofalseand an error is set aterrors.root.server.
Props
All props are optional. Any other <form> attributes, such as id, className, or aria-*, are passed to the rendered <form> element.
| Name | Type | Description | Example |
|---|---|---|---|
control | Control | control object provided by invoking useForm. Optional when using FormProvider. |
|
action | string | (formData: FormData) => unknown | URL to submit to, or Since v7.84.0 a function (Server Action style) invoked with the submission's FormData. |
|
method | "post" | "put" | "delete" | HTTP method used when action is a URL string. GET is not supported since submissions are sent as a request body. Defaults to post. |
|
encType | "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain" | "application/json" | Sets the request's Content-Type and how the body is encoded. See Request body. By default the submission is sent as FormData. |
|
children | React.ReactNode | Form contents. |
|
render | ({ submit }) => React.ReactNode | Render prop for headless use, such as React Native. When render is set, no <form> element is rendered. Call submit() to start the submission. |
|
onSubmit | FormSubmitHandler | Function invoked after successful validation and before the request is sent. Receives { data, event, method, formData, formDataJson }. |
|
onSuccess | ({ response }) => void | Function called after a request to a URL action returns a status that passes validateStatus. |
|
onError | ({ response } | { error }) => void | Function called when the request fails. You get { response } when the status fails validateStatus, and { error } when fetch or a function action throws.An error is also set on formState.errors, with root.server as the key. |
|
headers | Record<string, string> | Request headers object. |
|
validateStatus | (status: number) => boolean | Function to validate the response status code. Defaults to status >= 200 && status < 300. |
|
onSubmit payload
| Name | Type | Description |
|---|---|---|
data | TTransformedValues | The validated form values. If you use a resolver, these are the transformed values. |
event | React.BaseSyntheticEvent | The submit event. This is undefined when you call submit() from render without an event. |
method | "post" | "put" | "delete" | The method prop. |
formData | FormData | data encoded as FormData. This is the same body that is sent by default. |
formDataJson | string | data encoded with JSON.stringify. This is the body that is sent for JSON requests. |
Request body
The request body depends on encType, or on a Content-Type header in headers. The header name is matched without case sensitivity.
encType / Content-Type | Body sent |
|---|---|
| (not set) | FormData. The browser sets the multipart/form-data header and boundary. |
multipart/form-data | FormData. Form does not set the Content-Type header, so the browser can add the boundary. |
application/json | formDataJson (the result of JSON.stringify(data)). |
application/x-www-form-urlencoded | URLSearchParams made from the FormData. Files are sent as their file name only. |
text/plain | One name=value line for each field. Files are sent as their file name only. |
When values are encoded as FormData, nested objects and arrays use dot-notation keys, for example user.name and items.0.title. undefined values are skipped. Each file in a FileList is appended under the same key.
-
Data serialization: The
<Form>component focuses on form submission, not custom data serialization. Type-specific transformations (e.g.Date,Dayjs, or custom objects) should be handled at the application level before submission. We aim to keep the component lightweight and avoid introducing additional serialization rules. -
If you want to prepare or omit submission data, please use
handleSubmitoronSubmit.const { handleSubmit, control } = useForm();const onSubmit =(data) => callback(prepareData(data))<form onSubmit={handleSubmit(onSubmit)} />// or<FormonSubmit={({ data }) => {console.log(data)}}/> -
Progressive enhancement is only applicable to SSR frameworks.
const { handleSubmit, control } = useForm({progressive: true});<Form onSubmit={onSubmit} control={control} action="/api/test" method="post"><input {...register("test", { required: true })} /></Form>// Renders<form action="/api/test" method="post"><input required name="test" /></form> -
Formneeds acontrol. Pass it as a prop, or renderForminside aFormProvider. -
onSuccessis only called for a URL stringaction. Whenactionis a function, there is no response to check, so treat the end of the submission as success, for example withformState.isSubmitSuccessful. -
onErroris only for request errors. Validation errors do not callonError; read them fromformState.errors. -
The
<form>element getsnoValidateonly after it mounts. Before hydration, the browser's native validation (fromprogressive: true) protects the form. After hydration, React Hook Form validates it.
Form component vs handleSubmit
Both APIs validate with the rules configured in useForm. Choose based on who should own the request:
| Use | Best when |
|---|---|
Form | You want React Hook Form to encode the data, send the request, and expose request success or failure through formState. It also supports progressive enhancement and function actions. |
handleSubmit | You need complete control over the submission, such as retries, cancellation, multiple requests, custom serialization, or integration with a data-fetching library. |
Use Form with a URL action for a conventional API endpoint, a function action for a Server Action-style workflow, or no action when your onSubmit callback sends the request itself.
Examples
React Web
import { useForm, Form } from "react-hook-form"function App() {const {control,register,formState: { isSubmitSuccessful, errors },} = useForm({// progressive: true, optional prop for progressive enhancement})return (<div>{/* Use action prop to make post submission with formData */}<Formaction="/api"control={control}onSuccess={() => {alert("Success")}}onError={() => {alert("error")}}><input {...register("name")} />{isSubmitSuccessful && <p>Form submit successful.</p>}{errors?.root?.server && <p>Form submit failed.</p>}<button>submit</button></Form>{/* Manual form submission */}<Formcontrol={control}onSubmit={async ({ formData, data, formDataJson, event }) => {await fetch("api", {method: "post",body: formData,})}}><input {...register("test")} /> <button>submit</button></Form></div>)}
Send JSON and show the submission state
import { useForm, Form } from "react-hook-form"type FormValues = {email: string}function App() {const {control,register,formState: { isSubmitting, isSubmitSuccessful, errors },} = useForm<FormValues>()return (<Formcontrol={control}action="/api/subscribe"encType="application/json" // sends JSON.stringify(data)headers={{ Authorization: `Bearer ${token}` }}onError={async ({ response }) => {// response is undefined when the network request itself failedif (response) console.error(await response.text())}}><input {...register("email", { required: "Email is required" })} />{errors.email && <p>{errors.email.message}</p>}{errors.root?.server && (<p role="alert">Something went wrong ({errors.root.server.type || "network error"}).</p>)}{isSubmitSuccessful && <p>Thanks for subscribing!</p>}<button disabled={isSubmitting}>{isSubmitting ? "Submitting..." : "Subscribe"}</button></Form>)}
errors.root.server.type holds the response status as a string, such as "500". It is an empty string when fetch or a function action throws.
Server Action
// actions.ts"use server"export async function createPost(formData: FormData) {await db.post.create({ data: { title: String(formData.get("title")) } })}
"use client"import { useForm, Form } from "react-hook-form"import { createPost } from "./actions"export function PostForm() {const {control,register,formState: { isSubmitSuccessful, errors },} = useForm({ progressive: true })return (<Form control={control} action={createPost}><input {...register("title", { required: true })} />{errors.root?.server && <p role="alert">Could not save the post.</p>}{isSubmitSuccessful && <p>Post created.</p>}<button>Create</button></Form>)}
When action is a function, Form does not render method or encType on the <form> element, because React manages those for Server Actions.
With FormProvider
import { useForm, FormProvider, Form, useFormContext } from "react-hook-form"function NameInput() {const { register } = useFormContext()return <input {...register("name")} />}function App() {const methods = useForm()return (<FormProvider {...methods}>{/* No control prop: Form reads it from FormProvider */}<Form action="/api"><NameInput /><button>submit</button></Form></FormProvider>)}
File upload
import { useForm, Form } from "react-hook-form"function App() {const { control, register } = useForm()return (<Form control={control} action="/api/upload" encType="multipart/form-data"><input type="text" {...register("title")} />{/* each selected file is appended under "attachments" */}<input type="file" multiple {...register("attachments")} /><button>Upload</button></Form>)}
React Native
import { useForm, Form } from "react-hook-form"function App() {const {control,register,formState: { isSubmitSuccessful, errors },} = useForm()return (<Formaction="/api"control={control}render={({ submit }) => (<View>{isSubmitSuccessful && <Text>Form submit successful.</Text>}{errors?.root?.server && <Text>Form submit failed.</Text>}<Button onPress={() => submit()} /></View>)}/>)}
Tips
- Pick one place to send the request. Use
actionto letFormcallfetch, or leave outactionand send the request inonSubmit. If you set both,onSubmitruns first and then the request is sent. This is useful for logging or analytics, but not for changing the payload. - Return a promise from
onSubmit.Formwaits for it before it sends the request, andisSubmittingstaystruewhile it waits. IfonSubmitthrows, the request is not sent and the error is not caught for you. - Show server errors with
errors.root.server. Request errors are stored there and not on a field, so they do not block the next submission. Root errors are cleared each time the form is submitted again. - Do not set
Content-Typefor file uploads. The browser must create themultipart/form-dataheader with its boundary. LeaveencTypeunset or set it tomultipart/form-data, and do not add aContent-Typeheader. - Use
validateStatusfor APIs that use non-2xx codes on purpose. For example,validateStatus={(status) => status < 500}treats a409 Conflictas a success, so you can read the body inonSuccess. - Need more control? If you need retries, request cancellation, or a data-fetching library, use
handleSubmiton a plain<form>.Formis a convenience for simple submissions.
Troubleshooting
The form sends two requests
Why it happens
An action URL tells Form to send a request after onSubmit finishes. If onSubmit also calls fetch or a mutation, both requests run.
Fix
Choose one request owner. Pass action and use onSubmit only for preparation or analytics, or omit action and send the request yourself in onSubmit.
onError is not called when validation fails
Why it happens
onError handles request failures. When client-side validation fails, no request is sent, so request callbacks do not run.
Fix
Render field validation errors from formState.errors. Use onError for a rejected function action, a failed network request, or a response status rejected by validateStatus.
A file upload arrives empty or cannot be parsed
Why it happens
Setting Content-Type: multipart/form-data by hand omits the boundary generated by the browser. Setting JSON encoding instead serializes form values as JSON and cannot send the file bytes from a FileList.
Fix
For file uploads, leave Content-Type unset (and leave encType unset or use multipart/form-data) so the browser sends FormData with the correct boundary.