React.js: Use 'react-hook-form' and 'zod' for implementing the Form

In this small post, I am going to discuss the Forms validation feature in React.js using the react-hoof-form and zod libraries.

Why the Form Validations is important?

Form validation is required to ensure that user inputs are correct, complete, and secure before submission, this helps to prevent invalid data from being sent to the backend and improves the user experience. The form validation results in following benefits:

  • Data accuracy: To Prevents incomplete or incorrect data submissions.
  • Security: This help to blocks malicious inputs e.g., SQL injection, XSS.
  • User Experience: The improvements in user experience reliefs the end user about the error messages foe wrong or invalid data.
  • Enforcement of Business Logic: This makes sure that the core business logic is actually enforces business logic the username must not be repeated or the rules for string password are follows or even the date ranges are set properly.
Following are some of the common validation needs:


Validation Type Example Rule Why It’s Important
Required fields Name, EmailAddress must not be empty Prevents incomplete submissions
Length checks Password ≥ 12 characters Enforces security standards
Pattern checks Email must match regex ^[^@]+@[^@]+\.[^@]+$ Ensures correct format
Numeric validation Age must be a number between 21–65 Enforces business rules
Cross-field validation Confirm Password must match Password Prevents mismatched inputs
Custom rules Username must be unique Aligns with application-specific logic

In React.js we have various options to perform the form validations, some of them are as follows:
  • Write your own logic to validate each form field by using onBlur or onChange events. This is good approach because you are implementing each validation per the business needs, but you end-up writing huge and complex code. 
  • Use third-party libraries like:
    • react-hook-form with Yup or Zod
    • Formik with Yup
Using the third-party library is good option, of-course there exist a learning curve, but the standard validation relieves you from writing complex logic and since these libraries already offers built-in validation support, the form validation is concise and highly cohesive.     

What is Formik?

This is a form state management library for React. These uses React state internally to track form values, errors, and touched fields. This offers simple APIs those are easy to learn. Formik has good documentation and great community support. This works well with Yup for validation. But the downside is that it can be heavy on performance for large forms because it needs re-renders frequently. Since Formik offers built-in components, it has more boilerplate compared to React Hook Form.

What is React Hook Form (RHF)?

This is Lightweight form library built on React hooks. It uses uncontrolled components and refs, minimizing re-renders. This is High performance, especially with large forms. This has minimal boilerplate, integrates easily with UI libraries. The best part is react-hook-form works seamlessly with both Yup and Zod. Some downside is it is slightly steeper learning curve if you’re used to controlled components. This needs deep understanding of React hooks.

What is Yup?

Yus is Schema-based validation library. This Defines validation rules using a fluent API like Yup.string().UserName().required. This is widely used, integrates smoothly with Formik and RHF. It has the clear and declarative syntax. Since it is written in JavaScript, so type safety is weaker compared to Zod. In the TypeScript separate types needs to be maintained to use Yup for schema validations.

What is Zod?

The Zod is TypeScript-first schema validation library. This defines schemas that serve as both runtime validators and TypeScript types. This offers Strong type safety. This Works well with RHF for end-to-end type-safe forms. This is More modern and flexible than Yup. The Zod has slightly different syntax that may feel less familiar to developers used to Yup.

What is Hook Form Resolver?

@hookform/resolvers is an official package for React Hook Form that is used to integrate external validation libraries (ike Yup, Zod, Joi, Vest, Ajv, and more to handle the form. Advantage offered by this is instead of writing manual validation logic, a resolver can be used to connect the schema-based validation directly to React Hook Form 

The following table sown the easy and point based comparison across the above discussed libraries:
Library Best For Performance Type Safety
Formik Beginners, simple forms Medium Weak😐
React Hook Form Large forms, performance-critical High😊 Medium
Yup Declarative rules, wide adoption Medium Weak😑
Zod TypeScript projects, type safety High😊 Strong

 

The Implementation
  
The React Project is created using Vite.  The following command is used to create the project:

npm create vite@latest my-app -- --template react-ts

Since I have used the react-hook-form and zod these packages are installed using the following command:

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

In the react application, add a new component named customerfformcomponent.tsx. In this component we will validate Customer Information like Name, Address, Age, Email, DateOfBirth, RegistratrionDate, etc. The Names and Address are mandatory, the Email must be unique, DateOfBirth must be less than RegistrationDate, the age must be minimum 18. The code for the for, is as follows.

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

// 1. Define schema with proper date handling
const customerFormSchema = z
  .object({
    customerName: z.string().nonempty("Customer name is required"),
    address: z.string().nonempty("Address is required"),

    email: z
      .string()
      .email("Invalid email format")
      .refine(
        async (value) => {
          try {
            if (!value.endsWith(".com")) return false;

            const res = await fetch(
              `https://localhost:7250/api/check-email?email=${value}`
            );
            if (!res.ok) return false;

            const data = await res.json();
            return data.IsAvailable === true;
          } catch {
            return false;
          }
        },
        { message: "Email is not available" }
      ),

    // Convert string → Date
    dateOfBirth: z
      .string()
      .nonempty("Date of birth is required")
      .transform((val) => new Date(val)),

    registrationDate: z
      .string()
      .nonempty("Registration date is required")
      .transform((val) => new Date(val)),

    age: z.number().min(18, "Age must be at least 18"),
  })
  .refine(
    (data) => data.registrationDate > data.dateOfBirth,
    {
      message: "Registration date must be after date of birth",
      path: ["registrationDate"],
    }
  );

// 2. Infer schema type
type CustomerDataFormModel = z.infer<typeof customerFormSchema>;

const CustomerFormComponent = () => {
  // 3. Bind schema to form
  const {
    register,
    handleSubmit,
    formState: { errors, isValid },
  } = useForm<CustomerDataFormModel>({
    resolver: zodResolver(customerFormSchema),
    mode: "onBlur",
    reValidateMode: "onBlur",
    defaultValues: {
      customerName: "",
      address: "",
      email: "",
      age: 0,
    },
  });

  const onSubmit = (data: CustomerDataFormModel) =>
    console.log(`Form submitted: ${JSON.stringify(data)}`);

  return (
    <div>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label>Customer Name:</label>
          <input type="text" {...register("customerName")} />
          {errors.customerName && (
            <p style={{ color: "red" }}>{errors.customerName.message}</p>
          )}
        </div>

        <div>
          <label>Address:</label>
          <input type="text" {...register("address")} />
          {errors.address && (
            <p style={{ color: "red" }}>{errors.address.message}</p>
          )}
        </div>

        <div>
          <label>Email:</label>
          <input type="email" {...register("email")} />
          {errors.email && (
            <p style={{ color: "red" }}>{errors.email.message}</p>
          )}
        </div>

        <div>
          <label>Date of Birth:</label>
          <input type="date" {...register("dateOfBirth")} />
          {errors.dateOfBirth && (
            <p style={{ color: "red" }}>{errors.dateOfBirth.message}</p>
          )}
        </div>

        <div>
          <label>Registration Date:</label>
          <input type="date" {...register("registrationDate")} />
          {errors.registrationDate && (
            <p style={{ color: "red" }}>{errors.registrationDate.message}</p>
          )}
        </div>

        <div>
          <label>Age:</label>
          <input type="number" {...register("age", { valueAsNumber: true })} />
          {errors.age && (
            <p style={{ color: "red" }}>{errors.age.message}</p>
          )}
        </div>

        <div>
          <button type="submit" disabled={!isValid}>
            Save
          </button>
        </div>
      </form>
    </div>
  );
};

export default CustomerFormComponent;
In the above code, the zod is used to define and validate then schema. We have used z.infer, this is a TypeScript utility that extracts the TypeScript type from a Zod schema. It allows to use the same schema both for runtime validation and compile-time type safety, ensuring that the data is consistent across validation and typing. 
Since we want to validate the Email address as unique, we need to add custom asynchronous validation logic to access API to do the validation check. To add this custom logic, we have used refine() method. Similarly, we have used refine() method for date validations i.e. DateOfBirth and Registration date. Validation Rules are bound to form using the useForm() Hook. 

What is useForm() Hook?

The useForm() hook in React Hook Form is the core API that is used for managing form state, validation, and submission. It offers a lightweight and high performant way to handle forms without uncontrolled re-renders, while offering a rich set of features for customization. The useForm() hook does the following: 
  • It Initializes form state.
  • Then it Registers inputs and tracks their values.
  • Further it handles validation using built-in resolvers like Yup/Zod.
  • Finally, it manages submission, reset, and error handling.
The following table explain all properties of the useForm() Hook.
Feature Description Example Snippet
register Connects input fields to the form state register("emailaddress")
handleSubmit Wraps the submit handler with validation logic handleSubmit(onSubmit)
formState Provides the metadata like errors, isDirty, isValid, isSubmitting formState.errors.emailaddress
watch Subscribes to input values in real time for changes watch("dateofbirth")
setValue Programmatically sets a values of teh field setValue("age", 18)
getValues Retrieves current form values getValues()
reset Resets form values and state reset({ emailaddress: "", password: "" })
trigger Manually triggers validation trigger("emailaddress")
unregister Removes a field from tracking unregister("fieldName")




The ASP.NET Core API code is as follows.

using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.PropertyNamingPolicy = null;
});

builder.Services.AddCors(options =>
{
     options.AddPolicy("cors", builder =>
     {
         builder.AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader();
     });
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();

app.UseCors("cors");

 

app.MapGet("/api/check-email", (string email) =>
{
    if (email.Equals("mahesh.sabnis@myorg.com"))
    {
        return Results.Ok(new { IsAvailable = false });
    }
    else
    {
        return Results.Ok(new { IsAvailable = true });
    }
});

app.Run();
Now, run the ASP.NET Core Application and then the react application. In the browser navigate to http://localhost:7153. In react form, keep pressing TAB for each input without entering and data, you will see validations as shown in Figure 1.


Figure 1: All Validations
Enter data all input elements, enter mahesh.sabnis@myorg.com and enter DateofBirth value greater than RegistrationDate, the validations errors will be displayed on form as shown in Figure 2.



Figure 2: The Date and Email validations
   
The asynchronous validation is executed for the Email and Date validation for DatOfBirth and RegsitrationDate are also executed. Now enter all valid data, the form will remove all error messages and Save button will be enabled as shown in Figure 3.



Figure 3: The Valid Data

Conclusion: Every React form should validate required fields, and enforce formats like email/phone, check lengths, and apply custom business rules. Libraries like React Hook Form + zod make this process clean and scalable.

Popular posts from this blog

ASP.NET Core 8: Creating Custom Authentication Handler for Authenticating Users for the Minimal APIs

ASP.NET Core 7: Using PostgreSQL to store Identity Information

Uploading Excel File to ASP.NET Core 6 application to save data from Excel to SQL Server Database