DEV Community

Cover image for Validating forms in React Apps with Zod
Guilherme Cheng
Guilherme Cheng

Posted on

Validating forms in React Apps with Zod

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.

form draft

// 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;
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

We also need to change our useForm, passing its resolver:

const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<SchemaProps>({
    resolver: zodResolver(validationSchema)
  });
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

The form, with active errors, should look like this:

active errors form

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' }),
});
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)