Go Back

React Hook Form with Zod

In this article we'll build a basic form using React Hook Form with zod validation.

We'll focus on the basics of React Hook Form. This guide won't cover styling or more advanced use cases such as working with external UI libraries and using "Controller".

Why React Hook Form?

React Hook Form is an amazing lightweight tool. It allows to handle form state and validate it very easily. It is also more performant than normal controlled forms. It doesn't re-render the component on every input change, unlike the useState approach. Try to put some data in this form and look at the re-renders counter.

Count: 0

See? That's a lot of unnecessary re-renders. That's because this form uses useState. Now try the same with this form which uses React Hook Form

Count: 0

That's much better.

With all of that, it is very easy to understand. Let's build a form like this.

Installing dependencies

We need to install these three dependencies to begin.

npm i react-hook-form zod @hookform/resolvers

Creating a schema

First, let's declare the form schema. It's a good idea to start with this because it makes creating the form easier.

const FormSchema = z.object({
  email: z.string().email({ message: 'Invalid email address' }),
  username: z
    .string()
    .min(4, { message: 'Username must be 4 or more characters long' })
    .max(16, { message: 'Username must be 16 or fewer characters long' }),
  password: z
    .string()
    .min(4, { message: 'Password must be 4 or more characters long' })
    .max(16, { message: 'Password must be 16 or fewer characters long' }),
});

Zod validation is very easy to understand. It allows you to check if user provided correct email address or to check if the username or password is between 4 and 16 and provide specific message if not. I strongly encourage you to take a look at zod documentation. for more specific use cases.

To use it with React Hook Form we need to make a type from this schema.

type TFormSchema = z.infer<typeof FormSchema>;

Now TFormSchema is the type of our form.

Starting with React Hook Form

Let's start creating our form. At first we'll take a look at useForm hook. It provides for us many properties and functions. For now let's focus on two of them.

const { register, handleSubmit } = useForm();

You probably already know what they do:

To connect our zod validation to this form we need to use schema type as a generic.

const { register, handleSubmit } = useForm<TFormSchema>({
  resolver: zodResolver(FormSchema),
});

useForm takes an object as its first argument, and in this case, we need to specify which resolver we want to use. zodResolver function which takes previously created schema as a argument comes from @hookform/resolvers/zod

Creating a form.

We're ready to create our form. Let's start with just a skeleton.

export default function Form() {
  const { register, handleSubmit } = useForm<TFormSchema>({
    resolver: zodResolver(FormSchema),
  });
 
  return (
    <form>
      <input placeholder="Email" />
      <input placeholder="Username" />
      <input placeholder="Password" type="password" />
      <button type="submit">Sign in</button>
    </form>
  );
}

Caution

If you are using Next.js don't forget to put "use client" at the top of your file.

But how to use register and handleSubmit?

register is a function that take the name of our input as its first argument. The name must match the schema, but don't worry — it's type-safe, and TypeScript will assist you. It return all necessary properties which we want on our input. It should look like this:

<input placeholder="Email" {...register('email')} />

handleSubmit takes a function as a first argument which we want to execute at form submission. It will pass all input values as a first argument to it.

export default function Form() {
  const { register, handleSubmit } = useForm<TFormSchema>({
    resolver: zodResolver(FormSchema),
  });
 
  const submitForm: SubmitHandler<TFormSchema> = ({
    email,
    username,
    password,
  }) => {
    // your form submission logic goes here
  };
 
  return (
    <form onSubmit={handleSubmit(submitForm)}>
      <input placeholder="Email" {...register('email')} />
      <input placeholder="Username" {...register('username')} />
      <input placeholder="Password" type="password" {...register('password')} />
      <button type="submit">Sign in</button>
    </form>
  );
}

SubmitHandler type is from React Hook Form as well.

Errors

Our form works as expected but we're not doing anything with errors. To handle it we'll use a formState object from useForm hook. It has errors object which contains errors for every registered input.

const {
  register,
  handleSubmit,
  formState: { errors },
} = useForm<TFormSchema>({
  resolver: zodResolver(FormSchema),
});

Now we can check if an error occurred for a specific input and display it conditionally.

return (
  <form onSubmit={handleSubmit(submitForm)}>
    <input placeholder="Email" {...register('email')} />
    {errors.email?.message && <p>{errors.email.message}</p>}
    <input placeholder="Username" {...register('username')} />
    {errors.username?.message && <p>{errors.username.message}</p>}
    <input placeholder="Password" type="password" {...register('password')} />
    {errors.password?.message && <p>{errors.password.message}</p>}
    <button type="submit">Sign in</button>
  </form>
);

You may also want to reset inputs values after submission. To do it just use reset function provided by useForm hook at the end of the submitForm.

That's all! We built out first form together using React Hook Form. It provides many more things than that. For more specific use cases and more information check out docs here.

Our finished form look like this:

Count: 0

We achieved it with this code:

import { useForm, SubmitHandler } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
 
const FormSchema = z.object({
  email: z.string().email({ message: 'Invalid email address' }),
  username: z
    .string()
    .min(4, { message: 'Username must be 4 or more characters long' })
    .max(16, { message: 'Username must be 16 or fewer characters long' }),
  password: z
    .string()
    .min(4, { message: 'Password must be 4 or more characters long' })
    .max(16, { message: 'Password must be 16 or fewer characters long' }),
});
 
type TFormSchema = z.infer<typeof FormSchema>;
 
export default function Form() {
  const {
    register,
    handleSubmit,
    formState: { errors },
    reset,
  } = useForm<TFormSchema>({
    resolver: zodResolver(FormSchema),
  });
 
  const submitForm: SubmitHandler<TFormSchema> = ({
    email,
    username,
    password,
  }) => {
    // your form submission logic goes here
    reset();
  };
 
  return (
    <form onSubmit={handleSubmit(submitForm)}>
      <input placeholder="Email" {...register('email')} />
      {errors.email?.message && <p>{errors.email.message}</p>}
      <input placeholder="Username" {...register('username')} />
      {errors.username?.message && <p>{errors.username.message}</p>}
      <input placeholder="Password" type="password" {...register('password')} />
      {errors.password?.message && <p>{errors.password.message}</p>}
      <button type="submit">Sign in</button>
    </form>
  );
}

Don't forget to style it in a user-friendly way. I used styling from shadcn/ui but it is not provided in code snippets. For more take a closer look at React Hook Form documentation.

Thanks for reading!

Go Back
REACT.JSFORMS

LinkedInGitHubTwitterBluesky