DEV Community

Cover image for Building a Type-Safe Form in NextJs with Zod
Mohammad Ezzeddin Pratama
Mohammad Ezzeddin Pratama

Posted on

Building a Type-Safe Form in NextJs with Zod

Forms are a crucial part of any web application, and building them in a type-safe manner ensures fewer bugs and better maintainability. In this blog post, we'll create a complete form in React using TypeScript and Zod for validation. All the code will be contained in a single file for simplicity.

What is Zod?
Zod is a TypeScript-first schema declaration and validation library. It allows you to define schemas for your data, which can then be used to validate inputs in a type-safe manner. This makes Zod an excellent choice for validating forms in React applications.

Setting Up the Project
If you haven't already, you'll need to install React, TypeScript, and Zod in your project:

npm install react react-dom typescript zod
Enter fullscreen mode Exit fullscreen mode

Now, let's create a simple form that collects a user's first name, last name, and email, with validation for each field.

The Code
Here's the complete code for the form component:

typescript

import React, { useState } from 'react';
import { z } from 'zod';

// Define the schema using Zod
const formSchema = z.object({
  firstName: z.string().min(1, "First Name is required"),
  lastName: z.string().min(1, "Last Name is required"),
  email: z.string().email("Invalid email address"),
});

// TypeScript type derived from the Zod schema
type FormData = z.infer<typeof formSchema>;

export default function ZodForm() {
  // State to manage form input values
  const [formData, setFormData] = useState<FormData>({
    firstName: '',
    lastName: '',
    email: '',
  });

  // State to manage form errors
  const [formErrors, setFormErrors] = useState<Partial<FormData>>({});

  // Handle input changes
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = event.target;
    setFormData({ ...formData, [name]: value });
  };

  // Handle form submission
  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    // Validate form data using Zod
    const result = formSchema.safeParse(formData);

    if (!result.success) {
      // If validation fails, set errors
      const errors: Partial<FormData> = {};
      result.error.errors.forEach((error) => {
        if (error.path[0]) {
          errors[error.path[0] as keyof FormData] = error.message;
        }
      });
      setFormErrors(errors);
    } else {
      // If validation succeeds, clear errors and handle form submission
      setFormErrors({});
      alert('Form submitted successfully!');
      console.log(result.data);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="max-w-sm mx-auto mt-10">
      <div className="mb-4">
        <label htmlFor="firstName" className="block text-sm font-medium text-gray-700">
          First Name
        </label>
        <input
          type="text"
          id="firstName"
          name="firstName"
          value={formData.firstName}
          onChange={handleInputChange}
          className="mt-1 block w-full p-2 border border-gray-300 rounded-md"
        />
        {formErrors.firstName && (
          <p className="text-red-500 text-sm mt-1">{formErrors.firstName}</p>
        )}
      </div>

      <div className="mb-4">
        <label htmlFor="lastName" className="block text-sm font-medium text-gray-700">
          Last Name
        </label>
        <input
          type="text"
          id="lastName"
          name="lastName"
          value={formData.lastName}
          onChange={handleInputChange}
          className="mt-1 block w-full p-2 border border-gray-300 rounded-md"
        />
        {formErrors.lastName && (
          <p className="text-red-500 text-sm mt-1">{formErrors.lastName}</p>
        )}
      </div>

      <div className="mb-4">
        <label htmlFor="email" className="block text-sm font-medium text-gray-700">
          Email
        </label>
        <input
          type="email"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleInputChange}
          className="mt-1 block w-full p-2 border border-gray-300 rounded-md"
        />
        {formErrors.email && (
          <p className="text-red-500 text-sm mt-1">{formErrors.email}</p>
        )}
      </div>

      <button
        type="submit"
        className="w-full bg-blue-500 text-white py-2 px-4 rounded-md"
      >
        Submit
      </button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Explanation
Let's break down what's happening in the code:

  1. Defining the Schema with Zod We start by defining a schema using Zod. This schema enforces the rules for each form field. For instance, firstName and lastName are required fields, and email must be a valid email address.

typescript

const formSchema = z.object({
  firstName: z.string().min(1, "First Name is required"),
  lastName: z.string().min(1, "Last Name is required"),
  email: z.string().email("Invalid email address"),
});
Enter fullscreen mode Exit fullscreen mode
  1. Managing State We manage the form data using React's useState hook. This allows us to keep track of the user's input and any validation errors.

typescript

const [formData, setFormData] = useState<FormData>({
  firstName: '',
  lastName: '',
  email: '',
});
Enter fullscreen mode Exit fullscreen mode
const [formErrors, setFormErrors] = useState<Partial<FormData>>({});
Enter fullscreen mode Exit fullscreen mode
  1. Handling Input Changes The handleInputChange function updates the state as the user types in each input field.

typescript

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  const { name, value } = event.target;
  setFormData({ ...formData, [name]: value });
};
Enter fullscreen mode Exit fullscreen mode
  1. Handling Form Submission When the form is submitted, we validate the data using Zod's safeParse method. If the validation fails, we display error messages next to the relevant fields. If the validation succeeds, we proceed with form submission (in this case, simply logging the data and displaying an alert).

typescript

const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
  event.preventDefault();

  const result = formSchema.safeParse(formData);

  if (!result.success) {
    const errors: Partial<FormData> = {};
    result.error.errors.forEach((error) => {
      if (error.path[0]) {
        errors[error.path[0] as keyof FormData] = error.message;
      }
    });
    setFormErrors(errors);
  } else {
    setFormErrors({});
    alert('Form submitted successfully!');
    console.log(result.data);
  }
};
Enter fullscreen mode Exit fullscreen mode

Conclusion
In this post, we created a fully functional and type-safe form using React, TypeScript, and Zod. This setup ensures that your form handles user input and validation in a robust manner. By leveraging Zod, you can easily extend this approach to more complex forms and validation rules.

Top comments (2)

Collapse
 
noartem profile image
Noskov Artem

This can be made easier by using libraries such as Conform

Collapse
 
ezzeddinp profile image
Mohammad Ezzeddin Pratama

thats sounds new for me, Thanks man👍🏻