Zod
Zod is a TypeScript-first schema declaration and validation library. It allows you to define validation schemas for data structures, such as objects, strings, numbers, arrays, etc. Zod validates the data according to the schema and provides clear error messages if the data doesn't match the expected structure.
Basically it helps us to define a schema for a data structure and then validate that data structure values against the schema and give us proper error.
Key Features of Zod:
Type-safe: Zod integrates seamlessly with TypeScript, ensuring that the types of data are validated correctly and that type inference works as expected.
Declarative Validation: You define a schema that describes the expected structure of the data, and Zod will automatically validate the data against that schema.
Error Handling: Zod provides detailed, easy-to-understand error messages when validation fails, making it simple to debug validation issues.Composability: You can combine Zod schemas to create more complex data structures, making it easy to reuse and compose validation logic.
Asynchronous Validation: Zod supports asynchronous validation for scenarios such as checking if a value exists in a database.
Okay, Let's move to the coding part, best practice with zod validation library.
For me it is to create a custom hook and then use it everywhere according to my need, you can explore your best choice and let me know more in comments so I can learn new ways too.
Now move to the code, so here we are going to create a custom hook named useFormValidation which will return us an object containing three fields
errors: This will contains an objects with key of form data and error associated with as value.
validateField: It is a function which will validate a particular field of a form. It takes two params- first one is name of field which we want to validate and the other one is the value of that field.
validateForm: It is also a function which will validate the whole form in one go. It takes only one param that is form data in the object form.
Okay now its time to create our custom hook as we explained above. First we are going to install zod in our react + typescript project using command
npm install zod
After installing the zod we will create a file name form-validation.tsx and put this code there
import { z, ZodSchema, ZodObject } from "zod";
import { useState, useCallback } from "react";
// Define a generic type for the schema
type SchemaType<T> = ZodObject<{ [K in keyof T]: ZodSchema<T[K]> }>;
/**
* Custom Hook for form validation using Zod
*/
export const useFormValidation = <T extends Record<string, any>>(
schema: SchemaType<T>
) => {
const [errors, setErrors] = useState<Record<string, string>>({});
/**
* Validates a single field based on the schema
*/
const validateField = useCallback(
(name: any, value: any) => {
try {
const fieldSchema = schema.shape[name];
if (!fieldSchema) return;
const singleFieldSchema = z.object({ [name]: fieldSchema });
singleFieldSchema.parse({ [name]: value });
setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors[name as string];
return newErrors;
});
return true;
} catch (error) {
if (error instanceof z.ZodError) {
const fieldError = error.errors[0]?.message;
setErrors((prev) => ({
...prev,
[name as string]: fieldError,
}));
return false;
}
}
},
[schema]
);
/**
* Validates the entire form based on the schema
*/
const validateForm = useCallback(
(data: T) => {
try {
schema.parse(data);
setErrors({});
return { success: true, errors: null };
} catch (error) {
if (error instanceof z.ZodError) {
const newErrors = Object.fromEntries(
error.errors.map(({ path, message }) => [path[0], message])
);
setErrors(newErrors);
return { success: false, errors: newErrors };
}
return { success: false, errors: { form: "Validation failed" } };
}
},
[schema]
);
return {
errors,
validateField,
validateForm,
};
};
Let's explain each part
Code Explanation
1. Type Definition (UseZodValidationProps)
type SchemaType<T> = ZodObject<{ [K in keyof T]: ZodSchema<T[K]> }>;
This type defines the shape of the props that the useZodValidation hook will receive. The hook expects a schema prop, which is a ZodSchema (a validation schema from the Zod library). The T represents a generic type for form data (the schema type).
2. State for Errors (useState)
const [errors, setErrors] = useState<Record<string, string>>({});
errors is a state variable that will store validation errors for each field. It's an object where each key is a field name, and the value is the corresponding error message.
setErrors is used to update this state.
3. validateField Function
const validateField = useCallback(
(name: any, value: any) => {
try {
const fieldSchema = schema.shape[name];
if (!fieldSchema) return;
const singleFieldSchema = z.object({ [name]: fieldSchema });
singleFieldSchema.parse({ [name]: value });
setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors[name as string];
return newErrors;
});
return true;
} catch (error) {
if (error instanceof z.ZodError) {
const fieldError = error.errors[0]?.message;
setErrors((prev) => ({
...prev,
[name as string]: fieldError,
}));
return false;
}
}
},
[schema]
);
validateField is called when you want to validate a single field dynamically (e.g., on each input change).
Parameters:
- **name: **The name of the field to validate (e.g., "email"). value: The current value of the field (e.g., the input value).
Inside the function:
- singleFieldSchema.parse({ [name]: value }); This uses Zod's parse method to validate the specific field (name) against the schema. The schema is designed to validate the entire object, so we pass a partial object with just the field we're validating. If validation fails we extract the error message from the catch block and update the errors state with the field's error message. If the field is valid, we remove any previous error for that field from the errors state.
4. validateForm Function
const validateForm = useCallback(
(data: T) => {
try {
schema.parse(data);
setErrors({});
return { success: true, errors: null };
} catch (error) {
if (error instanceof z.ZodError) {
const newErrors = Object.fromEntries(
error.errors.map(({ path, message }) => [path[0], message])
);
setErrors(newErrors);
return { success: false, errors: newErrors };
}
return { success: false, errors: { form: "Validation failed" } };
}
},
[schema]
);
return {
errors,
validateField,
validateForm,
};
};
validateForm is used to validate the entire form (all fields) at once.
Parameters:
- data: The form data (an object) that needs to be validated against the schema.
Inside the function:
- parse(data): This validates the entire form data. If validation fails, it formats the error and creates an errors object containing error messages for each invalid field. The setErrors function is then called to update the state with these errors. If validation passes, it clears any previous errors by setting setErrors({}) and returns true to indicate success.
5. Return Object
return { errors, validateField, validateForm };
The hook returns the following:
errors: The current errors object containing validation error messages for the form fields.
validateField: A function to validate a single field in real-time (for example, when the user types in a field).
validateForm: A function to validate the entire form (for example, when submitting the form).
How This Works in a Component
You can use this hook in your form components to manage field validation.
- validateField is used for validating individual fields in real-time (e.g., when a user types in a field).
- validateForm is used when submitting the form, to ensure that all fields are validated at once.
Let's see an example to use this hook in a form
Example Usage in a Component
import React, { useState } from "react";
import { z } from "zod";
import { useFormValidation } from "./hooks/form-validation";
// Define a validation schema using Zod
const schema = z.object({
name: z.string().min(3, "Name must be at least 3 characters"),
email: z.string().email("Invalid email address"),
age: z.preprocess(
(val) => Number(val),
z.number().min(18, "Must be at least 18")
),
});
const MyForm = () => {
const { errors, validateField, validateForm } = useFormValidation(schema);
const [formData, setFormData] = useState({
name: "",
email: "",
age: 18,
});
const onChangeHandler = (field: keyof typeof formData, value: any) => {
setFormData((prev) => ({ ...prev, [field]: value }));
validateField(field, value); // Real-time field validation
};
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Validate the entire form using formData
const res = validateForm(formData);
console.log(res);
if (res.success) {
console.log("Form Data:", formData);
}
};
return (
<form onSubmit={onSubmit} className="space-y-4 p-4 border rounded-md w-80">
<div>
<label>Name:</label>
<input
value={formData.name}
onChange={(e) => onChangeHandler("name", e.target.value)}
className="border p-1 rounded w-full"
/>
{errors.name && <p className="text-red-500">{errors.name}</p>}
</div>
<div>
<label>Email:</label>
<input
value={formData.email}
onChange={(e) => onChangeHandler("email", e.target.value)}
className="border p-1 rounded w-full"
/>
{errors.email && <p className="text-red-500">{errors.email}</p>}
</div>
<div>
<label>Age:</label>
<input
type="number"
value={formData.age}
onChange={(e) => onChangeHandler("age", e.target.value)}
className="border p-1 rounded w-full"
/>
{errors.age && <p className="text-red-500">{errors.age}</p>}
</div>
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Submit
</button>
</form>
);
};
export default MyForm;
Top comments (0)