Handling Form Validations in React with React Hook Form and Zod

Handling Form Validations in React with React Hook Form and Zod

Form validation in ReactJS/NextJS

Introduction

Form validations are critical in web applications for ensuring data integrity and providing a consistent user experience. Handling form validations in React can be complicated and time-consuming at times. The process, however, can be simplified and made more efficient with the right tools and libraries.

In this article, we will look at how to handle form validations in React with the help of two powerful libraries: React Hook Form, Zod, and TypeScript. React Hook Form is a lightweight and adaptable form library with an easy-to-use API for managing form state and validations. Zod is a schema validation library that makes it simple to define and validate data structures.

We can create robust and dependable form validation systems in React applications by combining React Hook Form and Zod. Let us get started and look at the key concepts, techniques, and code examples for dealing with form validations effectively.

For the demo, we will build a basic form with validations and basic stylings using Sass (see the image below). This demo creates the app using the Nextjs React Framework because React recommends using popular React-powered frameworks in the community to create a new React app, of which Nextjs is one. Read here for more info.

Setting Up React Hook Form and Zod

Before we can handle form validations, we must first install React Hook Form and Zod in our project. Assuming you already have a React or Nextjs project set up and your form created, proceed as follows:

  1. Install the necessary dependencies:
npm install react-hook-form zod @hookform/resolvers

2. Import the required components and hooks:

import { useForm } from 'react-hook-form'; 
import { zodResolver } from '@hookform/resolvers/zod'; 
import { z } from 'zod';

3. Define your form schema using Zod:

Our validation schema will include the username, email, password, and password confirmation because our demo app will validate a form that registers a new user.

const schema = z.object({ 
    username: z.string().min(3, {message: 'Username must be at least 3 characters'}), 
    email: z.string().min(1, {message: 'Email is required'}).email('Invalid email address'), 
    password: z.string().min(6, {message: 'Password must be at least 6 characters'}), 
    confirmPassword: z.string().min(6, {message: 'Password must be at least 6 characters'}) 
    })

//extract the inferred type from schema 
type ValidationSchemaType = z.infer

4. Set up the form and validation resolver:

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

With React Hook Form and Zod set up, we are ready to handle form validations in our React application.

Basic Form Validation

Let us begin with the fundamentals of form validation with React Hook Form and Zod. We will go over validating individual form fields, displaying error messages, and dealing with form submissions.

  1. Registratering form fields:

The register function in the React Hook Form is used to register form fields, and it takes as an argument the form field's attribute name or the schema name.

<input type="text" {...register('name')} />
"use client";

import React from 'react'
import styles from './form.module.scss'
import { useForm, SubmitHandler } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const Form = () => {

  const schema = z.object({
    username: z.string().min(3, {message: 'Username must be at least 3 characters'}),
    email: z.string().min(1, {message: 'Email is required'}).email('Invalid email address'),
    password: z.string().min(6, {message: 'Password must be at least 6 characters'}),
    confirmPassword: z.string().min(6, {message: 'Password must be at least 6 characters'})
  }).refine((data) => data.password === data.confirmPassword, {
    path: ['confirmPassword'],
    message: 'Passwords does not match'
  })

  //extract the inferred type from schema
  type ValidationSchemaType = z.infer<typeof schema>

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

  return (
    <form className={styles.form_main}>
      <label htmlFor="username">
        Username:
        <input type="text" placeholder='username goes here...' {...register('username')} />
      </label>
      <label htmlFor="email">
        Email:
        <input type="email" placeholder='email goes here...' {...register('email')} />
      </label>

      <label htmlFor="password">
        Password:
        <input type="password" placeholder='password goes here...' {...register('password')} />
      </label>

      <label htmlFor="confirmPassword">
        Confirm Password:
        <input type="password" placeholder='Confirm password' {...register('confirmPassword')} />
      </label>
      <button type='submit'>Login</button>
    </form>
  )
}

export default Form

2. Displaying error messages:

The React Hook Form returns an errors object that is destructured from the formState. The errors give us access to the form errors and return the specified validation constraint message for each registered form field.

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

We can then conditionally render an error message declared in the validation schema for each form field. I have also created a CSS class called error_input to make the input border red when incorrect information is entered.

{errors.username && <span>{errors.username.message}</span>}
<form className={styles.form_main}>
      <label htmlFor="username">
        Username:
        <input type="text" placeholder='username goes here...' {...register('username')} className={errors.username && styles.error_input}/>
        {errors.username && (
          <span className={styles.error}>{errors.username?.message}</span>
        )}
      </label>
      <label htmlFor="email">
        Email:
        <input type="email" placeholder='email goes here...' {...register('email')}  className={errors.email && styles.error_input}/>
        {errors.email && (
          <span className={styles.error}>{errors.email?.message}</span>
        )}
      </label>


      <label htmlFor="password">
        Password:
        <input type="password" placeholder='password goes here...' {...register('password')}  className={errors.password && styles.error_input}/>
        {errors.password && (
          <span className={styles.error}>{errors.password?.message}</span>
        )}
      </label>

      <label htmlFor="confirmPassword">
        Confirm Password:
        <input type="password" placeholder='Confirm password' {...register('confirmPassword')}  className={errors.confirmPassword && styles.error_input}/>
        {errors.confirmPassword && (
          <span className={styles.error}>{errors.confirmPassword?.message}</span>
        )}
      </label>
      <button type='submit'>Login</button>
   </form>

3. Handling form submission:

We create a custom form submit handler function called onSubmit and print the form data to the console.

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

We then pass it as an argument to the handleSubmit function provided by the React Hook form.

"use client";

import React from 'react'
import styles from './form.module.scss'
import { useForm, SubmitHandler } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const Form = () => {

  const schema = z.object({
    username: z.string().min(3, {message: 'Username must be at least 3 characters'}),
    email: z.string().min(1, {message: 'Email is required'}).email('Invalid email address'),
    password: z.string().min(6, {message: 'Password must be at least 6 characters'}),
    confirmPassword: z.string().min(6, {message: 'Password must be at least 6 characters'})
  }).refine((data) => data.password === data.confirmPassword, {
    path: ['confirmPassword'],
    message: 'Passwords does not match'
  })

  type ValidationSchemaType = z.infer<typeof schema>

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

  // Form submit handler
  const onSubmit: SubmitHandler<ValidationSchemaType> = (data) => {
    console.log(data)
  }

  return (
    <form className={styles.form_main} onSubmit={handleSubmit(onSubmit)}>
      // other codes
    </form>
  )
}

export default Form

In this example, we register the ‘username’, ‘email," and "passwords" fields using the register function provided by React Hook Form. We then display the error message if the field has any errors. Finally, we handle form submission by calling the onSubmit function when the form is submitted. See the demo below.

Some Advanced Form Validation Techniques

Let’s explore some of these techniques:

  1. Cross-Field Validation:

The refine custom validator can be used to cross-validate the password and the confirmed password; it takes in the validation function and form data and performs a check if the password is equal to the confirmed password with the error path as the confirmPassword.

// Cross validating the password & confirm password
const schema = z.object({
    username: z.string().min(3, {message: 'Username must be at least 3 characters'}),
    email: z.string().min(1, {message: 'Email is required'}).email('Invalid email address'),
    password: z.string().min(6, {message: 'Password must be at least 6 characters'}),
    confirmPassword: z.string().min(6, {message: 'Password must be at least 6 characters'})
  }).refine((data) => data.password === data.confirmPassword, {
    path: ['confirmPassword'],
    message: 'Passwords does not match'
  })

In this example, we validate that the ‘confirm password’ field matches the ‘password’ field. If the values don’t match, an error message will be displayed.

2. Async Validation:

const schema = z.object({
  email: z.string().email ("Invalid email address').refine(async (value) => {
    // Perform async validation logic (e.g., check if email exists in the database)
    // Return true if validation passes, false otherwise
  }, 'Email already exists'),
});

This can be useful when you need to validate data against an external source, such as checking if an email already exists in a database.

Conclusion

Handling form validations in React is essential for building robust and user-friendly web applications. With the combination of React Hook Form and Zod, we have explored how to simplify and streamline the process. We learned how to configure React Hook Form and Zod, perform basic and advanced form validations, deal with complex validation scenarios, and improve the user experience.

You can ensure that user inputs are validated according to your desired criteria by leveraging the power and flexibility of React Hook Form and Zod. This improves data integrity, decreases errors, and provides a more pleasant user experience. Experiment with these techniques and libraries in your next React project to effectively and efficiently handle form validations.

For further reading, visit the React hook form and Zod official documentation to learn more about the libraries and more advanced concepts.

The demo's GitHub repository is available here, where you can view the entire code as well as the CSS stylings.

Happy coding!