Skip to main content

7.2 - React Hook Form

React Hook Form is a library for handling forms in React. It's useful useForm custom Hook handles form state and validation. React Hook Form integrates well with TypeScript.


Why use React Hook Form?

React Hook Form is a library for handling forms in React. It simplifies things like:

  • Validation - Developers can add validation to sure user input data is correctly formatted.
  • Handling form state - Rather than manually handling the state of each individual field, with React Hook Forms the entire form shares one state.
  • Incorportating other forms libraries - Other libraries, such as those for validation like Zod, can be smoothly integrated with React Hook Forms.

React Hook Form also provides performance improvements by having smarter and more optimized re-rendering.


The useForm Hook

React Hook Form's core functionality comes from its useForm custom Hook. Recall that a custom Hook is basically just a function.

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

useForm returns a bunch of props. (See the useForm documentation for the full list).

For now, we'll focus on the handleSubmit and register props:

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

export default function App() {
const { register, handleSubmit } = useForm()

return (
<form>
.
.
.
</form>
)
}

handleSubmit

handleSubmit is a function that takes in a function. The passed-in function is what will happen when the form is submitted and deemed valid. For example, you could pass in a simple onSubmit function:

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

export default function App() {
const { register, handleSubmit } = useForm()
const onSubmit = (data) => console.log(data)
return (
<form onSubmit={ handleSubmit(onSubmit) }>
.
.
.
<button type="submit">Submit</button>
</form>
)
}

handleSubmit will:

  1. Validate the data when the form is submitted (more on this later).
  2. Call our onSubmit function, only after the validation passes.

Finally, we pass in handleSubmit to be the form’s onSubmit prop. This just says that when the form is submitted, call handleSubmit. As stated before, handleSubmit will then check if the data is valid, then call our onSubmit function.


register

register is a function that binds an input field to the form state:

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

export default function App() {
const { register, handleSubmit } = useForm()
//...
return (
<form onSubmit={ handleSubmit(onSubmit) >
<input type="text" {...register("name")} />

<select {...register("category")}>
<option value="">Select...</option>
<option value="A">Category A</option>
<option value="B">Category B</option>
</select>

<button type="submit">Submit</button>
</form>
)
}

Validation with Register

register also lets you apply validation rules, which will be checked by handleSubmit.

Some register validation rules include:

  • required: The field is required.
  • min/maxLength: The field must be at least/at most a certain number of characters.
  • pattern: The field must adhere to a certain regex patter.
  • validate: The field must return true when passed into a given function, which returns true if valid, and otherwise returns an error message.

See the full list of validation rules you can provide to register in the documentation.

These rules are checked by handleSubmit later when form is submitted.

Example

This example also shows how to pass in an error message. Displaying error messages will be discussed in the next section.

<input type="text" {...register("username", {
required: "Username is required",
minLength: {
value: 6,
message: "Username must have >=6 characters"
},
pattern: {
value: "/^[a-zA-Z]",
message: "Username must start with a letter"
},
validate: (value) => {
if (value.contains(" ")) {
return "Username cannot contain spaces";
}
return true;
}
})} />

Handling and Displaying Error Messages

Say the form is submitted, but the data doesn't pass validation. handleSubmit will store any errors in the form state.

To display error messages, you will:

  1. Get errors from formState from useForm.
  2. Use conditional rendering to display an error message.

Step 1: Access Errors from Form State

In addition to register and handleSubmit, the useForm custom Hook can also return a formState prop. This is similar to how the built-in useState Hook returns a state variable as one of its props. formState is an object that covers the state of the entire form, including all fields in it, and contains properties that track information on the form. (See the documentation for a full list of formState's properties).

One of formState's properties is errors. errors is an object that contains the errors for each form field.

Access errors from formState from useForm like so:

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

export default function App() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm()
...
}

Step 2: Conditionally Render Error Messages

Next, use conditional rendering to display an error message. Here, when the form is submitted with a blank username, “Username is required” is displayed under where you type in a username (after the form is submitted):

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

export default function App() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm()
...
return (
<form ...>
<input type="text" {...register("username", {
required: "Username is required"
})} />
{errors.username && (
<p>{errors.username.message</p>
)}
</form>
)
}

The below snippet conditionally renders the error message by checking if errors.registered_name is present (i.e. not null, undefined, or any other falsy value). If so, then the expression evaluates to the second operand, which is the JSX element containing our error message. This element is then rendered.

{errors.registered_name && <>{errors.registered_name.message}</>}

TypeScript and Typing

TypeScript meshes well with React Hook Form by standardizing our form data and making sure inputted data conforms to expected typing. To add typing to our form with TypeScript, make the following simple changes.


Step 1: Create a custom Type

The custom Type should basically list out all input fields in your form and their type.

type Inputs = {
name: string
pet: string
}

Here, we named the custom Type Inputs. It contains two properties, name and pet, both of which are string. name might


Step 2: Pass the custom Type into useForm

The useForm Hook is a generic function. It can accept a type argument that defines the shape of the form's data.

Pass in the custom Type to useForm:

type Inputs = {
name: string
pet: string
}

export default function App() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<Inputs>()

...
}

Step 3: Import the SubmitHandler Type

React Hook Form has a custom type called SubmitHandler, which is used to define the type of the function that will handle form submissinos.

Import the SubmitHandler type from React Hook Form and apply it to the function passed into handeSubmit. Based on our old example, we'll apply this type to the simple onSubmit function we wrote:

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

type Inputs = {
name: string
pet: string
}

export default function App() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<Inputs>()

const onSubmit: SubmitHandler<Inputs> = (data) => {
console.log(data)
}

...
}

We also pass in our custom Inputs type to SubmitHandler. This ensures that the onSubmit function receives form data that matches the Input type we defined.


Overall Reference

Here is an overall reference:

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

type Inputs = {
name: string
pet: string
}

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

return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="name">Name</label>
<input type="text" id="name" {...register("name", { required: "Name Required" })} />
{errors.name && <p>errors.name.message</p>}

<label htmlFor="pet">Choose a pet:</label>
<select id="pet" {...register("pet")} />
<option value="">---Choose an option---</option>
<option value="dog">Dog</option>
<option value="cat">Cat</option>
<option value="hamster">Hamster</option>
</select>
{errors.pet && <p>errors.pet.message</p>}

<button type="submit">Submit</button>
</form>
)
}