React Hook Form with Zod Validation: Complete Guide
Form validation is a crucial aspect of building modern React applications. React Hook Form combined with Zod provides a powerful, type-safe solution for handling forms. In this guide, we'll learn how to implement robust form validation with file uploads and complex validation rules.
Form validation is a crucial aspect of building modern React applications. React Hook Form combined with Zod provides a powerful, type-safe solution for handling forms. In this guide, we'll learn how to implement robust form validation with file uploads and complex validation rules.
Installation
First, let's install the required packages:
npm install react-hook-form @hookform/resolvers zodCreating a Zod Schema
Let's create a product form schema with validation rules:
import * as z from "zod";
const productSchema = z.object({
name: z
.string()
.trim()
.min(1, { message: "Required" })
.min(2, { message: "Minimum 2 characters required" }),
categoryId: z.string().trim().min(1, { message: "Required" }),
sku: z.string().trim(),
description: z.string().trim(),
price: z.string().trim().min(1, { message: "Required" }),
cost: z.string().trim(),
stock: z.string().trim().min(1, { message: "Required" }),
minStock: z.string().trim(),
unit: z.string().trim(),
barcode: z.string().trim(),
product_image: z.preprocess((val) => {
if (!val) return null;
if (val instanceof FileList) {
const file = val.item(0);
return file ?? null;
}
return val;
}, z.instanceof(File).nullable().refine(
(file) => file !== null,
{ message: "Product image is required" }
)),
product_gallery: z.preprocess((val) => {
if (!val) return null;
if (val instanceof FileList) {
return Array.from(val);
}
return val;
}, z.array(z.instanceof(File)).optional().nullable()),
});
type ProductFormData = z.infer<typeof productSchema>;Setting Up React Hook Form
Now let's integrate React Hook Form with Zod validation:
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
function AddProduct() {
const {
register,
handleSubmit,
watch,
control,
formState: { errors, isSubmitting, isValid },
} = useForm<ProductFormData>({
defaultValues: {
name: "",
categoryId: "",
sku: "",
description: "",
price: "",
cost: "",
stock: "",
minStock: "",
unit: "pcs",
barcode: "",
product_image: null,
product_gallery: null,
},
resolver: zodResolver(productSchema),
mode: "all",
criteriaMode: "all",
});
const productImage = watch("product_image");
const canSubmit = isValid && productImage !== null;
const onSubmit = async (data: ProductFormData) => {
try {
const formData = new FormData();
formData.append("name", data.name);
formData.append("categoryId", data.categoryId);
formData.append("price", String(parseFloat(data.price) || 0));
formData.append("stock", String(parseInt(data.stock) || 0));
if (data.product_image) {
formData.append("product_image", data.product_image);
}
if (data.product_gallery && Array.isArray(data.product_gallery)) {
data.product_gallery.forEach((file) => {
formData.append("product_gallery", file);
});
}
// Submit form data
await submitForm(formData);
} catch (error) {
console.error("Error submitting form:", error);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Input
label="Product Name"
required
error={errors.name?.message as string}
{...register("name")}
/>
<Input
label="Price"
type="number"
step="0.01"
required
error={errors.price?.message as string}
{...register("price")}
/>
<FileInput
accept="image/*"
label="Product Image"
required
error={errors.product_image?.message as string}
{...register("product_image")}
/>
<button
type="submit"
disabled={isSubmitting || !canSubmit}
>
{isSubmitting ? "Submitting..." : "Submit"}
</button>
</form>
);
}Custom Input Components
Here's how to create reusable input components that work with React Hook Form:
import React, { forwardRef } from "react";
type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
label: string;
error?: string;
required?: boolean;
};
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, required = false, ...rest }, ref) => {
return (
<div className="flex flex-col gap-1">
<label className="block text-sm font-medium text-gray-900">
{label}
{required && <span className="text-red-500">*</span>}
</label>
<input
ref={ref}
{...rest}
className={`block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 ${error ? "outline-red-500" : ""}`}
/>
{error && <p className="text-red-500 text-xs mt-1">{error}</p>}
</div>
);
}
);
Input.displayName = "Input";
export default Input;Advanced Validation Patterns
Zod allows for complex validation patterns. Here are some examples:
// Email validation
const emailSchema = z.string().email({ message: "Invalid email address" });
// Number range validation
const priceSchema = z.string().refine(
(val) => {
const num = parseFloat(val);
return !isNaN(num) && num > 0;
},
{ message: "Price must be greater than 0" }
);
// Custom validation
const stockSchema = z.string().refine(
(val) => {
const num = parseInt(val);
return !isNaN(num) && num >= 0;
},
{ message: "Stock must be a non-negative number" }
);
// Conditional validation
const conditionalSchema = z.object({
hasDiscount: z.boolean(),
discount: z.string().optional(),
}).refine(
(data) => {
if (data.hasDiscount) {
return data.discount && parseFloat(data.discount) > 0;
}
return true;
},
{ message: "Discount is required when hasDiscount is true", path: ["discount"] }
);Best Practices
- Use Zod's
preprocessto transform data before validation (useful for FileList conversions) - Always provide clear error messages for better UX
- Use
mode: "all"to validate on blur and change - Leverage TypeScript's type inference with
z.infer - Create reusable validation schemas for consistency
Conclusion
React Hook Form with Zod provides a powerful, type-safe solution for form validation in React applications. The combination allows for complex validation rules, excellent TypeScript support, and great developer experience. This approach is especially useful in production applications where form validation is critical.