In modern React applications, form validation is a critical yet often overlooked aspect of user experience. When combined with TypeScript, ensuring type safety in form data becomes essential to prevent runtime errors and improve code reliability. This guide walks you through building a custom form validation hook using Zod — a powerful schema validation library — to create robust, maintainable forms without third-party dependencies.
Why Type-Safe Form Validation Matters
Manual form validation in JavaScript often leads to runtime errors and inconsistent data. TypeScript helps catch issues at compile time, but form data is dynamic and requires runtime checks. Zod bridges this gap by allowing you to define schemas that validate data against strict types, ensuring your application handles data correctly from the start.
Setting Up Zod for React TypeScript Projects
Start by installing Zod via npm or yarn:
- Run
npm install zodoryarn add zod - Create a schema for your form fields using Zod’s object syntax
- Use TypeScript to infer types from the schema for consistent type checking
For example, a user registration form schema might look like this:
import { z } from 'zod';
const registrationSchema = z.object({
email: z.string().email(),
password: z.string().min(8, 'Password must be at least 8 characters'),
terms: z.boolean().refine(value => value === true, 'You must accept terms')
});
This schema ensures email format, password length, and terms acceptance — all with detailed error messages.
Building a Custom Form Validation Hook with Zod in React TypeScript
Here’s how to create a reusable hook that handles validation:
- Use useState to manage form values and errors
- Implement a validation function that uses
schema.safeParse() - Return the current state and validation logic to your form components
Example implementation:
import { useState } from 'react';
import { z } from 'zod';
export const useZodForm = (schema: z.ZodType) => {
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
const validate = (data: unknown) => {
const result = schema.safeParse(data);
if (result.success) {
setErrors({});
return true;
}
const fieldErrors = result.error.errors.reduce((acc, err) => {
acc[err.path[0]] = err.message;
return acc;
}, {});
setErrors(fieldErrors);
return false;
};
return { values, errors, validate, setValues };
};
This hook centralizes validation logic, making it easy to reuse across forms while maintaining type safety.
Handling Validation Errors Gracefully
Displaying user-friendly errors is crucial for a good experience. Use the errors object returned by the hook to render messages next to each field:
<input
type="email"
value={values.email}
onChange={(e) => setValues({ ...values, email: e.target.value })}
/>
{errors.email && <span className="error">{errors.email}</span>}
For complex errors, consider mapping Zod error messages to more conversational language or grouping errors by field severity.
Practical Example: Registration Form
Here’s a complete registration form using the hook:
function RegistrationForm() {
const { values, errors, validate, setValues } = useZodForm(registrationSchema);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validate(values)) {
// Proceed with submission
}
};
return (
<form onSubmit={handleSubmit}>
<input
name="email"
value={values.email || ''}
onChange={(e) => setValues({ ...values, email: e.target.value })}
/>
{errors.email && <span className="error">{errors.email}</span>}
<input
name="password"
type="password"
value={values.password || ''}
onChange={(e) => setValues({ ...values, password: e.target.value })}
/>
{errors.password && <span className="error">{errors.password}</span>}
<label>
<input
type="checkbox"
checked={values.terms}
onChange={(e) => setValues({ ...values, terms: e.target.checked })}
/>
I accept the terms
</label>
{errors.terms && <span className="error">{errors.terms}</span>}
<button type="submit">Register</button>
</form>
);
}
This example demonstrates how the hook integrates with form fields, handles validation on submit, and displays errors clearly.
Benefits and Tradeoffs
Using a custom Zod hook offers significant advantages:
- Type safety across your entire form lifecycle
- No external dependencies beyond Zod
- Full control over validation logic and error handling
However, consider these tradeoffs:
- Requires manual implementation of features like debouncing or async validation
- Lacks built-in form state management found in libraries like React Hook Form
- More boilerplate code for complex forms
For simple forms, this approach provides lightweight, maintainable validation. For complex forms with advanced features, libraries like React Hook Form may be more efficient.
Conclusion
Building a custom form validation hook with Zod in React TypeScript ensures type-safe, maintainable forms while avoiding unnecessary dependencies. By centralizing validation logic and leveraging Zod’s powerful schema system, you reduce runtime errors and improve developer experience. Start by defining your Zod schema first — this sets the foundation for consistent data handling across your application. For further optimization, consider combining this hook with React Query for seamless form submission handling.