Zod is a lightweight and powerful validation library for JavaScript and TypeScript applications. It enables you to define the structure of your data through schemas and validate the input data against those schemas. Although it can be used with JavaScript applications, Zod works greatly with TypeScript. Zod can automatically generate TypeScript types from your schema, keeping the validation schema and TypeScript types in sync.
TypeScript, React Hook Form and Zod are a great combo that allows you to build powerful forms. As a result, you will learn how to validate your React Hook Form with Zod and TypeScript.
This article uses the form built in the "How to create and validate forms with React Hook Form (RHF)" article.
Prerequisites
You should have the following packages installed in your project before continuing further:
@hookform/resolvers zod
Also, you should have TypeScript installed and configured in your project.
Add Zod
Import the required dependencies at the top of the file:
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
zodResolver
is a function for integrating an external validation library like Zod with the React Hook Form library. The function takes the schema you define as an argument.
z
enables you to access Zod's functions and features.
Define the schema
Now you can start building the schema by creating a Zod object. An object schema allows you to define the structure and type of the form data. You can define multiple data inputs, specify their types, and add validation logic.
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
const FormSchema = z.object({
username: z.string(),
email: z.string(),
isAdmin: z.boolean(),
createdAt: z.coerce.date(),
});
The FormSchema
object defines a schema object with four fields and their types:
username
: stringemail
: stringisAdmin
: booleancreatedAt
: date
At the moment, the schema is simple. It only specifies the data types. Later, you will add validation logic, such as checking that the username doesn't contain special characters (@, !, etc.), for instance.
Infer TypeScript types from Zod schema
The official website mentions that Zod is "TypeScript-first schema validation with static type inference". That means you can automatically infer TypeScript types from a Zod schema.
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
const FormSchema = z.object({
username: z.string(),
email: z.string(),
isAdmin: z.boolean(),
createdAt: z.coerce.date(),
});
type FormInput = z.infer<typeof FormSchema>;
The FormInput
type now matches the schema. You can check it by hovering over the FormInput
type, which should show the data inputs and their types.
The possibility of automatically inferring TypeScript types from the Zod schema is beneficial because it ensures type safety and consistency between the two.
Pass Zod schema to React Hook Form
Until this point, you have the form schema and input types. But you are not using them. So let's pass them to the useForm
hook:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
const FormSchema = z.object({
username: z.string(),
email: z.string(),
isAdmin: z.boolean(),
createdAt: z.coerce.date(),
});
type FormInput = z.infer<typeof FormSchema>;
export default function Form() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormInput>({
resolver: zodResolver(FormSchema),
defaultValues: {
username: '',
email: '',
isAdmin: true,
createdAt: new Date(),
},
});
.....
}
In the above code, you:
- set up a React Hook Form instance using the
useForm
hook - pass a configuration object to the
useForm
hook as an argument - set the
resolver
property to the result of calling thezodResolver
function with theFormSchema
as an argument - define the default values for the form fields
The useForm
hook returns a couple of properties, such as register
, which allows you to register input and select elements for value tracking and validation or formState
, which is an object that contains information about the entire form state. It also returns the handleSubmit
function, which receives the form data if the form validation is successful. You use this method to handle the form submission.
Other properties are returned, but we're only interested in these three in this case. You can see all the properties in the documentation.
Build the JSX form
The last step involves writing the JSX code for the form. This is the complete code for the form:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
const FormSchema = z.object({
username: z.string(),
email: z.string(),
isAdmin: z.boolean(),
createdAt: z.coerce.date(),
});
type FormInput = z.infer<typeof FormSchema>;
export default function Form() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormInput>({
resolver: zodResolver(FormSchema),
defaultValues: {
username: '',
email: '',
isAdmin: true,
createdAt: new Date(),
},
});
return (
<form onSubmit={handleSubmit((d) => console.log(d))}>
<div>
<label htmlFor="username">Username</label>
<input id="username" {...register('username')} />
{errors?.username?.message && <p>{errors.username.message}</p>}
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" {...register('email')} />
{errors?.email?.message && <p>{errors.email.message}</p>}
</div>
<div>
<label htmlFor="isAdmin">IsAdmin</label>
<input id="isAdmin" type="checkbox" {...register('isAdmin')} />
</div>
<div>
<label htmlFor="createdAt">Creation Date</label>
<input id="createdAt" type="date" {...register('createdAt')} />
{errors?.createdAt?.message && (
<p>{errors.createdAt.message}</p>
)}
</div>
<button type="submit">Submit</button>
</form>
);
}
This is how the form works up to this point:
There are no validation rules, meaning the users can submit the form without entering any data. Moreover, they can also enter erroneous data and still submit the form. You want to avoid that happening, though. Instead, you want to make sure the users only enter valid data. So, in the next step, you'll add some validation logic.
You can see the complete form code with the styling in this gist.
Add validation logic
We want the form to display errors when users enter invalid data. To do that, you need to modify the form schema. Let's start with the username
input field. The username should:
- be a string
- have at least 4 characters
- have a maximum of 10 characters
- only contain letters, numbers and underscore
Zod provides a list of string validation functions you can use. We'll use the min
, max
, and regex
functions to validate the username
input field. These functions also allow you to pass a message
argument for a custom error message.
const FormSchema = z.object({
username: z
.string()
.min(4, { message: "The username must be 4 characters or more" })
.max(10, { message: "The username must be 10 characters or less" })
.regex(
/^[a-zA-Z0-9_]+$/,
"The username must contain only letters, numbers and underscore (_)"
),
});
If you try to enter an invalid username now, the form will not submit, and it will display the appropriate error message.
The email address should also be valid. Zod provides an email
function that deals with email addresses.
const FormSchema = z.object({
username: z
.string()
.min(4, { message: "The username must be 4 characters or more" })
.max(10, { message: "The username must be 10 characters or less" })
.regex(
/^[a-zA-Z0-9_]+$/,
"The username must contain only letters, numbers and underscore (_)"
),
email: z.string().email({
message: "Invalid email. Please enter a valid email address",
}),
});
The isAdmin
field can stay as it is. Therefore, we will not add any validation logic.
const FormSchema = z.object({
username: z
.string()
.min(4, { message: "The username must be 4 characters or more" })
.max(10, { message: "The username must be 10 characters or less" })
.regex(
/^[a-zA-Z0-9_]+$/,
"The username must contain only letters, numbers and underscore (_)"
),
email: z.string().email({
message: "Invalid email. Please enter a valid email address",
}),
isAdmin: z.boolean(),
});
The createdAt
field should have the following validation rules:
- the date cannot go past January 1 1920
- the date should be in the past
- the user should be 18 years or older
You can enforce those validation rules using the min
, max
, and refine
methods provided by Zod. In addition, the refine
method lets you create your custom validation logic. In this case, the code checks if the age of the user submitting the form is 18 or older. If it's younger, it displays the error message "You must be 18 years or older", and the user cannot submit the form.
const FormSchema = z.object({
username: z
.string()
.min(4, { message: "The username must be 4 characters or more" })
.max(10, { message: "The username must be 10 characters or less" })
.regex(
/^[a-zA-Z0-9_]+$/,
"The username must contain only letters, numbers and underscore (_)"
),
email: z.string().email({
message: "Invalid email. Please enter a valid email address",
}),
isAdmin: z.boolean(),
createdAt: z.coerce
.date()
.min(new Date(1920, 0, 1), {
message: "Date cannot go past January 1 1920",
})
.max(new Date(), { message: "Date must be in the past" })
.refine(
(date) => {
const ageDifMs = Date.now() - date.getTime();
const ageDate = new Date(ageDifMs);
const age = Math.abs(ageDate.getUTCFullYear() - 1970);
return age >= 18;
},
{ message: "You must be 18 years or older" }
),
});
You might've also observed the coerce
method. If you don't use the coerce
method, the form throws the following error "Expected date, received string".
The coerce
method lets you transform or modify the user input before it's validated against the schema. In this case, it converts the date string to a Date object by passing the input through new Date(input)
.
Final form
You now have a working React Hook Form with data validation using Zod and TypeScript.
In this article, you learnt how to:
- use react-hook-form with Zod and TypeScript
- create a schema
- infer the types from the Zod validation schema
- validate your form data using Zod
If you want to learn how to build a form with react-hook-form and use its built-in validation mechanisms, check the "How to create and validate forms with React Hook Form (RHF)" article.
If you're using React Hook Form and Zod, you might also be interested in the following articles: