After a lot of attempts, and experimenting different versions of it, I’ve come with what I'd say is the best way to validate forms. In this post I'll try to explain, step by step, how I do it. I hope this helps you in any way, as others helped me with their posts here.
So, let's go!
Starting setup
Lets start with a simple form, with some of the most requested data.
// component Form export function Form() { return ( <form className="form"> <label htmlFor="">Name</label> <input type="text" /> <label htmlFor="">Email</label> <input type="email" /> <label htmlFor="">Password</label> <input type="Password" /> <label htmlFor="">Number</label> <input type="number" /> <button type="submit" className="button"> Enviar </button> </form> ); } // main page import './App.css'; import { Form } from './components/Form'; function App() { return ( <div className="container"> <Form /> </div> ); } export default App;
Installing React Hook Form
$ npm install react-hook-form
Installing Zod
$ npm install zod
Adapting Form component to React Hook Form
To use React Hook Form, we will adequate our form according to the basic example usage, from the documentation:
- import register, handleSubmit and errors from useForm
- register must be inserted on every input, with its respective name
- call handleSubmit, from React Hook Form, at the 'onSubmit’ method of the form, receiving the ‘submitForm’, created by us. The submitForm function is where you will decide how to submit your data. In this example, we will only console the form data.
- if the input is not validated at submit, an error will be presented at 'errors’, to the respective not validated input.
import { useForm } from 'react-hook-form'; interface Inputs { name: string; email: string; password: string; number: number; } export function Form() { const { register, handleSubmit, formState: { errors }, } = useForm<Inputs>(); function submitForm() { console.log('submit'); } return ( <form className="form" onSubmit={handleSubmit(submitForm)}> <label htmlFor="">Name</label> <input type="text" {...register('name')} /> {errors?.name && 'erro'} <label htmlFor="">Email</label> <input type="email" {...register('email', { required: true })} /> {errors?.email && 'erro'} <label htmlFor="">Password</label> <input type="Password" {...register('password')} /> <label htmlFor="">Number</label> <input type="number" {...register('number')} /> <button type="submit" className="button"> Enviar </button> </form> ); }
Using Zod
Now, we will add Zod validation to the form. First, we need to install it, along with @hookform/resolvers, to correctly use Zod with React Hook Form:
Installing Zod
$ npm install zod $ npm install @hookform/resolvers
Creating the validation Schema
Now, we will create an object schema, and infer its type:
import { z } from 'zod'; import { zodResolver } from "@hookform/resolvers/zod"; const validationSchema = z.object({ name: z.string().min(1), email: z.string().min(1).email(), password: z.string().min(1), phone: z.string().min(1), }); type SchemaProps = z.infer<typeof validationSchema>;
We also need to change our useForm, passing its resolver:
const { register, handleSubmit, formState: { errors }, } = useForm<SchemaProps>({ resolver: zodResolver(validationSchema) });
Don't forget to put the error messages in the form!
// form should look something like this <form className="form" onSubmit={handleSubmit(submitForm)}> <div> <label htmlFor="">Name</label> {errors?.name && <span>{errors.name.message}</span>} </div> <input type="text" {...register('name')} /> <div> <label htmlFor="">Email</label> {errors?.email && <span>{errors.email.message}</span>} </div> <input type="email" {...register('email')} /> <div> <label htmlFor="">Password</label> {errors?.password && <span>{errors.password.message}</span>} </div> <input type="Password" {...register('password')} /> <div> <label htmlFor="">Phone Number</label> {errors?.phone && <span>{errors.phone.message}</span>} </div> <input type="string" {...register('phone')} /> <button type="submit" className="button"> Enviar </button> </form>
The form, with active errors, should look like this:
Customizing
Now, let's customize our validation. We will:
- insert custom error messages to each validation, of each property
- insert strong password validation (min 8 characters, one uppercase, one lowercase, one number and one special character)
- insert phone number validation (brazilian phone number)
// phone validation (Brazil) const phoneValidation = new RegExp( /^(?:(?:\+|00)?(55)\s?)?(?:\(?([1-9][0-9])\)?\s?)?(?:((?:9\d|[2-9])\d{3})\-?(\d{4}))$/ ); // Minimum 8 characters, at least one uppercase letter, one lowercase letter, one number and one special character const passwordValidation = new RegExp( /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/ ); const validationSchema = z.object({ name: z.string().min(1, { message: 'Must have at least 1 character' }), email: z .string() .min(1, { message: 'Must have at least 1 character' }) .email({ message: 'Must be a valid email', }), password: z .string() .min(1, { message: 'Must have at least 1 character' }) .regex(passwordValidation, { message: 'Your password is not valid', }), phone: z .string() .min(1, { message: 'Must have at least 1 character' }) .regex(phoneValidation, { message: 'invalid phone' }), });
For phone and password validations, we use the .regex, calling our validation, and passing the error message.
To sum up, this is the entire code of the component:
import { z } from 'zod'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; const phoneValidation = new RegExp( /^(?:(?:\+|00)?(55)\s?)?(?:\(?([1-9][0-9])\)?\s?)?(?:((?:9\d|[2-9])\d{3})\-?(\d{4}))$/ ); const passwordValidation = new RegExp( /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/ ); const validationSchema = z.object({ name: z.string().min(1, { message: 'Must have at least 1 character' }), email: z .string() .min(1, { message: 'Must have at least 1 character' }) .email({ message: 'Must be a valid email', }), password: z .string() .min(1, { message: 'Must have at least 1 character' }) .regex(passwordValidation, { message: 'Your password is not valid', }), phone: z .string() .min(1, { message: 'Must have at least 1 character' }) .regex(phoneValidation, { message: 'invalid phone' }), }); type SchemaProps = z.infer<typeof validationSchema>; export function Form() { const { register, handleSubmit, formState: { errors }, } = useForm<SchemaProps>({ resolver: zodResolver(validationSchema), }); function submitForm() { console.log('submit'); } return ( <form className="form" onSubmit={handleSubmit(submitForm)}> <div> <label htmlFor="">Name</label> {errors?.name && <span>{errors.name.message}</span>} </div> <input type="text" {...register('name')} /> <div> <label htmlFor="">Email</label> {errors?.email && <span>{errors.email.message}</span>} </div> <input type="email" {...register('email')} /> <div> <label htmlFor="">Password</label> {errors?.password && <span>{errors.password.message}</span>} </div> <input type="Password" {...register('password')} /> <div> <label htmlFor="">Phone number</label> {errors?.phone && <span>{errors.phone.message}</span>} </div> <input type="string" {...register('phone')} /> <button type="submit" className="button"> Enviar </button> </form> ); }
That's it guys! I hope you enjoyed reading it, and that it helped you understand a little about how it works.
Also, if you have any advices or comments on this, please leave a comment!