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
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>
);
}
Explanation
Let's break down what's happening in the code:
- 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"),
});
- 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: '',
});
const [formErrors, setFormErrors] = useState<Partial<FormData>>({});
- 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 });
};
- 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);
}
};
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)
This can be made easier by using libraries such as Conform
thats sounds new for me, Thanks manππ»