Form Validation In TypeScipt Projects Using Zod and React Hook Form

Strapi - Jul 18 - - Dev Community

Introduction

Form validation is an important aspect of software development. This is because it ensures type safety and helps preserve data integrity by preventing your application users from submitting inaccurate or incomplete information.

Imagine a form asking for an exact number of guests, yet a user enters a range. In this scenario, your application is not expecting a range. But imagine again that the inaccurate data gets to your database. This is not a behavior you want from your application.

Validation helps prevent such mismatches. Form validation can be implemented on either the client side (using JavaScript) or the server side.

In this article, you will learn how to build type-safe forms in a TypeScript project using the powerful combination of React-Hook-Form library for form management and Zod for robust validation on the client side.

Prerequisites

To follow along this article and understand the code, you will need to satisfy the following conditions:

Understanding Form Validation in TypeScript

In TypeScript, form validation uses the language's type system to create robust and reliable forms.

Principles of Form Validation

Form validation entails defining rules and checking for the data users enter into your application form. These rules (principles) can include various aspects, such as:

  • Required fields: Identifying fields that users must fill out before submitting the form.
  • Data types: Ensuring that data entered into specific fields adheres to the expected format (e.g., email address, phone number).
  • Value ranges: Limiting the range of acceptable values for certain fields (e.g., age within a specific range).
  • Data validity: Checking for specific criteria within the data (e.g., a valid password must meet minimum length requirements).

By implementing these validation checks, you prevent users from submitting incomplete or erroneous data, ultimately improving data quality and the functionality of your web application.

Introduction to Typescript's Type System and its Role in Form Validation

TypeScript's core strength lies in its type system. This system allows you to define the expected data types for variables, functions, and other aspects of your code. In the context of form validation, you can leverage types to define the structure of your form data.

For instance, you can create an interface or type alias that specifies the properties (fields) of your form and their corresponding data types.

Common Challenges in Form Validation and How Typescript Addresses Them

Here are some common challenges developers often encounter in form validation and how TypeScript helps you fix them:

  • Data type mismatches: TypeScript's static type checking identifies situations where the data entered does not match the expected type. This helps prevent runtime errors and ensures data consistency.
  • Incomplete data: Defining required fields as types forces users to fill them out before submission, reducing the likelihood of missing information.
  • Error-prone manual validation: By leveraging types and validation libraries that integrate with TypeScript, you automate the validation process, minimizing the chance of human error.

Introducing Zod Validation Library

Zod is a powerful TypeScript-first schema declaration and validation library with static type inference for TypeScript applications, which allows you to define the expected structure (schema), data types, and validation rules for your application's inputs.

Zod is a lightweight, yet powerful validation library that enables you to define the structure of your data through schemas. Although TypeScript-first, it also supports JavaScript validation.

001-zod.png

Zod supports many schema types, from primitive values like strings, numbers, and booleans to complex types like objects, arrays, and tuples. When TypeScript, React Hook Form, and Zod are combined, you can build robust and powerful forms. In the following sections, you will learn how to set up your form validation project.

Setting Up Your TypeScript Project

If you wish to quickly get started, proceed with cloning the GitHub repository.

First, install create-next-app globally by running:

npm i -g create-next-app
Enter fullscreen mode Exit fullscreen mode

After that, you can use create-next-app CLI to create a Next app.

Install and Create a React-Typescript Application

For this project, you will use Create Next App with the Typescript template to quickly bootstrap your React application. You will need to run this command to get started:

npx create-next-app@latest zod-ts-rhf --typescript --eslint
cd zod-ts-rhf
Enter fullscreen mode Exit fullscreen mode

The command above:

  • Uses the Create Next App command to create an application named zod-ts-rhf
  • Uses the --typescript flag to specify the template to use; in this case TypeScript.
  • Creates a Next.js application with support for TypeScript and all necessary configurations installed by default.
  • cd zod-ts-rhf changes directory into the zod-ts-rhf folder.

Ensure to make the following selection:

002-nextjs-installation.png

Configure Tailwind CSS

Inside the folder zod-ts-rhf, update your tailwind.config.ts with this code:

import type { Config } from "tailwindcss";

const config: Config = {
  content: [
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  mode: "jit",
  theme: {
    extend: {},
    screens: {
      xs: "480px",
      ss: "620px",
      sm: "768px",
      md: "1060px",
      lg: "1200px",
      xl: "1700px",
    },
  },
  plugins: [],
};
export default config;

Enter fullscreen mode Exit fullscreen mode

The above code adds responsive breakpoints to Tailwind CSS.

Install Zod

Next, you need to install the zod library and the React Hook Form resolver (@hookform/resolvers) by running this command inside the zod-ts-rhf directory:

npm i @hookform/resolvers zod
Enter fullscreen mode Exit fullscreen mode

In the next section, you will set up your form and learn how to get started using Zod.

Define Form Schemas with Zod

First, start with creating a Form component. To do this, create a components folder in the src directory, then create a Form.tsx file in the components directory, and then, paste this code:

We will be building a sign-up form with Zod validation. Paste the following code in the Form.tsx file

// ./src/components/Form.tsx

import React from "react";

export default function Form() {
  return (
    <div>
      <div>
        <div className="signup-1 flex items-center relative h-screen">
          <div className="overlay absolute inset-0 z-0 bg-black opacity-75"></div>
          <div className="container px-4 mx-auto relative z-10">
            <div className="sm:w-10/12 md:w-8/12 lg:w-6/12 xl:w-5/12 mx-auto">
              <div className="box bg-white p-6 md:px-12 md:pt-12 border-t-10 border-solid border-indigo-600">
                <h2 className="text-3xl text-gray-800 text-center">
                  Create Your Account
                </h2>

                <form>
                  <div className="signup-form mt-6 md:mt-12">
                    <div className="border-2 border-solid rounded flex items-center mb-4">
                      <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
                        <span className="far fa-user text-gray-500"></span>
                      </div>
                      <div className="flex-1">
                        <input
                          type="text"
                          placeholder="Username"
                          className="text-gray-700 h-10 py-1 pr-3 w-full"
                        />
                      </div>
                    </div>

                    <div className="border-2 border-solid rounded flex items-center mb-4">
                      <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
                        <span className="far fa-envelope text-gray-500"></span>
                      </div>
                      <div className="flex-1">
                        <input
                          type="text"
                          placeholder="E-mail"
                          className="text-gray-700 h-10 py-1 pr-3 w-full"
                        />
                      </div>
                    </div>

                    <div className="border-2 border-solid rounded flex items-center mb-4">
                      <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
                        <span className="fas fa-asterisk text-gray-500"></span>
                      </div>
                      <div className="flex-1">
                        <input
                          type="password"
                          placeholder="Password"
                          className="text-gray-700 h-10 py-1 pr-3 w-full"
                        />
                      </div>
                    </div>

                    <div className="border-2 border-solid rounded flex items-center mb-4">
                      <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
                        <span className="far fa-user text-gray-500"></span>
                      </div>
                      <div className="flex-1">
                        <input
                          type="text"
                          placeholder="Full name"
                          className="text-gray-700 h-10 py-1 pr-3 w-full"
                        />
                      </div>
                    </div>

                    <div className="border-2 border-solid rounded flex items-center mb-4">
                      <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
                        <span className="fa fa-hashtag text-gray-500"></span>
                      </div>
                      <div className="flex-1">
                        <input
                          type="number"
                          placeholder="Age"
                          className="text-gray-700 h-10 py-1 pr-3 w-full"
                        />
                      </div>
                    </div>

                    <p className="text-sm text-center mt-6">
                      By signing up, you agree to our{" "}
                      <a href="#" className="text-indigo-600 hover:underline">
                        Terms
                      </a>{" "}
                      and{" "}
                      <a href="#" className="text-indigo-600 hover:underline">
                        Privacy Policy
                      </a>
                    </p>

                    <div className="text-center mt-6 md:mt-12">
                      <button className="bg-indigo-600 hover:bg-indigo-700 text-white text-xl py-2 px-4 md:px-6 rounded transition-colors duration-300">
                        Sign Up{" "}
                        <span className="far fa-paper-plane ml-2"></span>
                      </button>
                    </div>
                  </div>
                </form>

                <div className="border-t border-solid mt-6 md:mt-12 pt-4">
                  <p className="text-gray-500 text-center">
                    Already have an account,{" "}
                    <a href="#" className="text-indigo-600 hover:underline">
                      Sign In
                    </a>
                  </p>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

Then, update the index.tsx file in the ./src/pages/index.tsx file with this code:

import Form from "@/components/Form";

export default function Home() {
  return (
    <>
      <Form />
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

The code snippets above renders the Form component in the index.tsx file corresponding to the index page since you are making use of the Next.js Pages Router. Subsequently, in this article, you will only focus on working on the Form.tsx file.

Start Application

Start your app by running the command below:

npm run dev
Enter fullscreen mode Exit fullscreen mode

You should see your Sign-up form by navigating to http://localhost:3000:

003-signup-form.png

In the next sections, you will walk through creating schema definitions for form fields and also, handling complex validation scenarios with Zod.

Create Schema Definitions for Form Fields

To get started with Zod validation in your form, you will need to import a couple of things at the beginning of your Form.tsx file:

  • zodResolver (from @hookform/resolvers/zod): This allows you to connect Zod's validation with React Hook Form. It expects your defined schema as input.
  • z (from zod): This import gives you access to all the functionalities and functions offered by Zod for building your validation schema.
// ./src/components/Form.tsx
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';

...
Enter fullscreen mode Exit fullscreen mode

Next, define your form schema. This form will have the following form fields:

  • username,
  • email,
  • password,
  • fullName,
  • age
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";

const FormSchema = z.object({
  username: z.string(),
  email: z.string(),
  password: z.string(),
  fullName: z.string(),
  age: z.number(),
});

type IFormInput = z.infer<typeof FormSchema>;

...


Enter fullscreen mode Exit fullscreen mode

The code snippet above uses Zod to define FormSchema to define a schema object that has five form fields with the following data types:

  • username: string,
  • email: string,
  • password: string,
  • fullName: string,
  • age: number

The IFormInput type uses Zod type inference to statically infer the TypeScript type from the FormSchema schema. This is an added advantage provided by Zod, which allows you to infer types rather than explicitly defining them.

Right now, the validation check by Zod is a simple one, it defines the data type for each form field.

In the next section, you will walk you through how to use Zod to add complex validation checks like defining the minimum and maximum length for the password field, checking that the username does not contain special characters, and checking that the age falls within a range.

By hovering over IFormInput in your code editor, you should see that IFormInput matches your Zod FormSchema schema.

004-zod-form-schema.png

Handling Complex Validation Scenarios with Zod

In this section, you will update the FormSchema schema to check for complex validations. To do that, update the FormSchema schema with this code below:

const FormSchema = z.object({
  username: z
    .string()
    .min(3, "Username must not be lesser than 3 characters")
    .max(25, "Username must not be greater than 25 characters")
    .regex(
      /^[a-zA-Z0-9_]+$/,
      "The username must contain only letters, numbers and underscore (_)",
    ),
  email: z.string().email("Invalid email. Email must be a valid email address"),
  password: z
    .string()
    .min(3, "Password must not be lesser than 3 characters")
    .max(16, "Password must not be greater than 16 characters"),
  fullName: z.string().min(3, "Name must not be lesser than 3 characters"),
  age: z.string().refine(
    (age) => {
      return Number(age) >= 18;
    },
    { message: "You must be 18 years or older" },
  ),
});

Enter fullscreen mode Exit fullscreen mode

In the above code, you are using the different Zod validation functions such as min(), max(), string(), email(), .refine(), and regex() to validate the different form fields. These functions also accept custom error messages in case the validation does not pass.

Specifically, you are doing the following for these form fields:

  • For username: You are using the min() function to make sure the characters are not below three and, then the max() function checks that the characters are not beyond twenty-five. You also make use of Regex to make sure the username must contain only letters, numbers, and underscore
  • For email: You use the email() function from Zod functions to check that the email is valid.
  • For password: Just like in the case of username, you are checking for the length using the min() and max() functions.
  • For fullName: You are using the min() function to set a minimum number of characters expected.
  • For age: you will use of the string() function, because, even though age should be a number. This is because Zod sees the values from the age input as strings. However, you will limit the values to number in the age input by using the <input type="number" />. You then use the Zod .refine() function to customize the validation logic to check that the age is greater than 18.

How to Create a Form with React Hook Form

Up until this section of the article, you have not started using both the IFormInput type and the FormSchema schema. In this section, you will learn how to pass the Zod schema to React Hook Form using the useForm hook:

Update the Form.tsx file with the following code, like so:

import { useForm } from "react-hook-form";
...

export default function Form() {
  ...

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

  const onSubmit = (data: IFormInput) => {
    console.log(data);
  };

  return (
    <div className="signup-1 flex items-center relative h-screen">
      <div className="overlay absolute inset-0 z-0 bg-black opacity-75"></div>
      <div className="container px-4 mx-auto relative z-10">
        <div className="sm:w-10/12 md:w-8/12 lg:w-6/12 xl:w-5/12 mx-auto">
          <div className="box bg-white p-6 md:px-12 md:pt-12 border-t-10 border-solid border-indigo-600">
            <h2 className="text-3xl text-gray-800 text-center">
              Create Your Account
            </h2>

            <form onSubmit={handleSubmit(onSubmit)}>
              <div className="signup-form mt-6 md:mt-12">
                <div className="border-2 border-solid rounded flex items-center mb-4">
                  <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
                    <span className="far fa-user text-gray-500"></span>
                  </div>
                  <div className="flex-1">
                    <input
                      {...register("username")}
                      type="text"
                      placeholder="Username"
                      className="text-gray-700 h-10 py-1 pr-3 w-full"
                    />
                  </div>
                </div>
                {errors?.username?.message && (
                  <p className="text-red-700 mb-4">{errors.username.message}</p>
                )}

                <div className="border-2 border-solid rounded flex items-center mb-4">
                  <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
                    <span className="far fa-envelope text-gray-500"></span>
                  </div>
                  <div className="flex-1">
                    <input
                      {...register("email")}
                      type="text"
                      placeholder="E-mail"
                      className="text-gray-700 h-10 py-1 pr-3 w-full"
                    />
                  </div>
                </div>
                {errors?.email?.message && (
                  <p className="text-red-700 mb-4">{errors.email.message}</p>
                )}

                <div className="border-2 border-solid rounded flex items-center mb-4">
                  <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
                    <span className="fas fa-asterisk text-gray-500"></span>
                  </div>
                  <div className="flex-1">
                    <input
                      {...register("password")}
                      type="password"
                      placeholder="Password"
                      className="text-gray-700 h-10 py-1 pr-3 w-full"
                    />
                  </div>
                </div>
                {errors?.password?.message && (
                  <p className="text-red-700 mb-4">{errors.password.message}</p>
                )}

                <div className="border-2 border-solid rounded flex items-center mb-4">
                  <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
                    <span className="far fa-user text-gray-500"></span>
                  </div>
                  <div className="flex-1">
                    <input
                      {...register("fullName")}
                      type="text"
                      placeholder="Full name"
                      className="text-gray-700 h-10 py-1 pr-3 w-full"
                    />
                  </div>
                </div>

                {errors?.fullName?.message && (
                  <p className="text-red-700 mb-4">{errors.fullName.message}</p>
                )}

                <div className="border-2 border-solid rounded flex items-center mb-4">
                  <div className="w-10 h-10 flex justify-center items-center flex-shrink-0">
                    <span className="fa fa-hashtag text-gray-500"></span>
                  </div>
                  <div className="flex-1">
                    <input
                      {...register("age")}
                      type="number"
                      placeholder="Age"
                      className="text-gray-700 h-10 py-1 pr-3 w-full"
                    />
                  </div>
                </div>
                {errors?.age?.message && (
                  <p className="text-red-700 mb-4">{errors.age.message}</p>
                )}

                <div className="text-center mt-6 md:mt-12">
                  <button
                    className="bg-indigo-600 hover:bg-indigo-700 text-white text-xl py-2 px-4 md:px-6 rounded transition-colors duration-300"
                    onClick={handleSubmit(onSubmit)}
                  >
                    Sign Up <span className="far fa-paper-plane ml-2"></span>
                  </button>
                </div>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

In the above code, you:

  • Start by importing useForm from the react-hook-form library. Other imports remain unchanged such as z and zodResolver.
  • 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 the zodResolver function with the FormSchema as an argument
  • You then passed the onSubmit() function as an argument to the React Hook Form handleSubmit() function and assigned it to the form's onSubmit event.

The useForm hook returns a couple of properties to manage your form. While it offers a wider range of functionalities, you will focus on three key ones for our Zod validation setup:

  • register: This registers your form's input. By using register, you tell React Hook Form to track the values entered by the input elements and integrate Zod's validation for them.
  • formState: This information about your entire form's state. It includes details like whether any validation errors.
  • handleSubmit: This function is fired when the user submits the form. But here's the catch: handleSubmit only receives the form data if it successfully passes Zod's validation checks. This way, you can be sure the data is clean before processing it further.

Running the application, you should see similar to the GIF below:

demo-gif.gif

Conclusion

In this article, you learned form validation using TypeScript, React Hook Form, and Zod. Here's a quick recap of the valuable takeaways:

You now understand the power of Zod for defining a schema – a blueprint for your form data. This schema controls the structure and data types for each form field, ensuring consistency and catching potential errors early. You also learned how Zod allows you to add validation logic, keeping your user-submitted data clean and reliable. By combining React Hook Form and Zod, you now know how to create robust and user-friendly forms.

Check out the complete code for this article on this GitHub repository. Check out the deployed site live.

If you still want to explore more on this topic, links to relevant articles and documentation are included below.

Additional Resources

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .