🛡️Complete Zod Validation Guide
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.
In simple terms: Zod helps us define a schema for a data structure, validate that data against the schema, and provide proper error messages when validation fails.
✨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.
🚨 Clear 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.
🚀Getting Started
Step 1: Install Zod
First, install Zod in your React + TypeScript project:
npm install zod
Step 2: Create the Custom Hook
Create a file named useZodValidation.ts (or .tsx):
🎯Custom Hook Implementation
import { useState } from "react";
import { z, ZodSchema } from "zod";
type UseZodValidationProps<T> = {
schema: ZodSchema<T>;
};
export const useZodValidation = <T extends Record<string, any>>({
schema,
}: UseZodValidationProps<T>) => {
const [errors, setErrors] = useState<Record<string, string>>({});
// Validate a single field dynamically as the user types
const validateField = (fieldName: keyof T, value: any) => {
// Create a partial object to validate just this field
const fieldData = { [fieldName]: value } as Partial<T>;
// Use safeParse to avoid throwing errors
const result = schema.safeParse(fieldData);
if (!result.success) {
// Extract the specific error for this field
const fieldError = result.error.formattedError;
const errorMessage = fieldError[fieldName as string]?._errors?.[0];
if (errorMessage) {
setErrors((prevErrors) => ({
...prevErrors,
[fieldName as string]: errorMessage
}));
}
} else {
// Remove error for this field if validation passes
setErrors((prevErrors) => {
const { [fieldName as string]: _, ...rest } = prevErrors;
return rest;
});
}
};
// Validate the entire form using the provided form data
const validateForm = (data: T): boolean => {
const result = schema.safeParse(data);
if (!result.success) {
// Format errors for all fields
const formattedErrors = result.error.formattedError;
const errorMap: Record<string, string> = {};
// Extract first error message for each field
Object.keys(data).forEach((key) => {
const fieldErrors = formattedErrors[key]?._errors;
if (fieldErrors && fieldErrors.length > 0) {
errorMap[key] = fieldErrors[0];
}
});
setErrors(errorMap);
return false;
}
// Clear all errors if validation passes
setErrors({});
return true;
};
// Clear all errors manually
const clearErrors = () => {
setErrors({});
};
// Clear specific field error
const clearFieldError = (fieldName: keyof T) => {
setErrors((prevErrors) => {
const { [fieldName as string]: _, ...rest } = prevErrors;
return rest;
});
};
return {
errors,
validateField,
validateForm,
clearErrors,
clearFieldError
};
};
📚Code Explanation
1. Type Definition
type UseZodValidationProps<T> = {
schema: ZodSchema<T>;
};
This type defines the props for our hook. It expects a schema which is a Zod validation schema. The generic T represents the shape of your form data.
2. State Management
const [errors, setErrors] = useState<Record<string, string>>({});
We maintain a state object where each key is a field name and the value is the corresponding error message.
3. Field Validation Function
🎯validateField Function
This function validates a single field in real-time (e.g., on input change):
- Takes fieldName and value as parameters
- Uses safeParse to validate without throwing errors
- Updates the errors state with the validation result
- Removes errors when validation passes
4. Form Validation Function
📋validateForm Function
This function validates the entire form at once (typically on submit):
- Takes the complete form data as a parameter
- Returns true if valid, false if invalid
- Updates errors state with all validation errors
- Clears all errors when validation passes
🏗️Complete Usage Example
Form Component with Validation
import React, { useState } from "react";
import { useZodValidation } from "./useZodValidation";
import { z } from "zod";
// Define a comprehensive validation schema
const userSchema = z.object({
name: z
.string()
.min(2, "Name must be at least 2 characters")
.max(50, "Name must be less than 50 characters")
.regex(/^[a-zA-Z\s]+$/, "Name can only contain letters and spaces"),
email: z
.string()
.email("Please enter a valid email address")
.min(1, "Email is required"),
age: z
.number()
.min(18, "Must be at least 18 years old")
.max(120, "Please enter a valid age"),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
"Password must contain uppercase, lowercase, and number"),
confirmPassword: z.string()
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});
// Infer the TypeScript type from the schema
type UserFormData = z.infer<typeof userSchema>;
const UserRegistrationForm: React.FC = () => {
const { errors, validateField, validateForm, clearErrors } =
useZodValidation({ schema: userSchema });
const [formData, setFormData] = useState<UserFormData>({
name: "",
email: "",
age: 18,
password: "",
confirmPassword: "",
});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleInputChange = (
field: keyof UserFormData,
value: string | number
) => {
setFormData((prev) => ({ ...prev, [field]: value }));
// Validate field in real-time
validateField(field, value);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
// Validate entire form
if (validateForm(formData)) {
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
console.log("Form submitted successfully:", formData);
// Reset form on success
setFormData({
name: "",
email: "",
age: 18,
password: "",
confirmPassword: "",
});
clearErrors();
alert("Registration successful!");
} catch (error) {
console.error("Submission failed:", error);
}
}
setIsSubmitting(false);
};
return (
<div className="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-6 text-center text-gray-800">
User Registration
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Name Field */}
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
Name
</label>
<input
id="name"
type="text"
value={formData.name}
onChange={(e) => handleInputChange("name", e.target.value)}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.name ? "border-red-500" : "border-gray-300"
}`}
placeholder="Enter your full name"
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name}</p>
)}
</div>
{/* Email Field */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.email ? "border-red-500" : "border-gray-300"
}`}
placeholder="Enter your email"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
)}
</div>
{/* Age Field */}
<div>
<label htmlFor="age" className="block text-sm font-medium text-gray-700 mb-1">
Age
</label>
<input
id="age"
type="number"
value={formData.age}
onChange={(e) => handleInputChange("age", parseInt(e.target.value) || 0)}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.age ? "border-red-500" : "border-gray-300"
}`}
min="0"
max="120"
/>
{errors.age && (
<p className="mt-1 text-sm text-red-600">{errors.age}</p>
)}
</div>
{/* Password Field */}
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
Password
</label>
<input
id="password"
type="password"
value={formData.password}
onChange={(e) => handleInputChange("password", e.target.value)}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.password ? "border-red-500" : "border-gray-300"
}`}
placeholder="Enter your password"
/>
{errors.password && (
<p className="mt-1 text-sm text-red-600">{errors.password}</p>
)}
</div>
{/* Confirm Password Field */}
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-1">
Confirm Password
</label>
<input
id="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={(e) => handleInputChange("confirmPassword", e.target.value)}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.confirmPassword ? "border-red-500" : "border-gray-300"
}`}
placeholder="Confirm your password"
/>
{errors.confirmPassword && (
<p className="mt-1 text-sm text-red-600">{errors.confirmPassword}</p>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isSubmitting}
className={`w-full py-2 px-4 rounded-md text-white font-medium transition-colors ${
isSubmitting
? "bg-gray-400 cursor-not-allowed"
: "bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
}`}
>
{isSubmitting ? "Registering..." : "Register"}
</button>
</form>
</div>
);
};
export default UserRegistrationForm;
💡Advanced Tips & Best Practices
🎯Pro Tips:
- Use Type Inference: Use z.infer<typeof schema> to automatically generate TypeScript types from your schema
- Real-time Validation: Call validateField on input change for immediate feedback
- Form Submission: Always call validateForm before submitting to ensure all data is valid
- Error Handling: Use conditional rendering to show errors only when they exist
- Loading States: Implement loading states during form submission for better UX
⚠️Common Gotchas:
- Remember to handle number inputs properly - convert strings to numbers
- Use safeParse instead of parse to avoid throwing errors
- For complex validation (like password confirmation), use .refine()
- Always clear errors when validation passes
🎉Benefits of This Approach
- Reusable: Use the same hook across different forms
- Type-Safe: Full TypeScript support with automatic type inference
- Flexible: Works with any Zod schema, simple or complex
- User-Friendly: Real-time validation provides immediate feedback
- Maintainable: Centralized validation logic that's easy to test and modify
Comments
Post a Comment