Zod Validation in [React + Typescript]

🛡️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

Popular Posts