Error Handling
Master validation and error management in Hero Hook Form.
Proper error handling improves user experience by providing clear, actionable feedback when something goes wrong.
Validation Types
Hero Hook Form supports both client-side and server-side validation.
Client-Side Validation
Client-side validation happens in the browser before form submission using Zod schemas.
Client-side validation provides immediate feedback and reduces server load by catching errors before submission.
import { z } from "zod";
const schema = z.object({
email: z.string().email("Please enter a valid email address"),
password: z.string()
.min(8, "Password must be at least 8 characters")
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
.regex(/[0-9]/, "Password must contain at least one number"),
confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});Server-Side Validation
Server-side validation occurs after form submission and requires manual error handling.
import { applyServerErrors } from "@rachelallyson/hero-hook-form";
const handleSubmit = async (data) => {
try {
const response = await fetch("/api/submit", {
method: "POST",
body: JSON.stringify(data),
});
if (!response.ok) {
const errorData = await response.json();
// Apply server errors to form
applyServerErrors(form, errorData.fieldErrors);
return;
}
// Success
console.log("Form submitted successfully");
} catch (error) {
console.error("Submission error:", error);
}
};Error Display Options
Inline Errors (Default)
Errors are displayed directly below each field.
<ZodForm
config={{ schema, fields }}
errorDisplay="inline" // Default
/>Toast Notifications
Errors are displayed as toast notifications.
<ZodForm
config={{ schema, fields }}
errorDisplay="toast"
/>Modal Errors
Errors are displayed in a modal dialog.
<ZodForm
config={{ schema, fields }}
errorDisplay="modal"
/>Custom Error Handling
Handle errors programmatically without automatic display.
<ZodForm
config={{ schema, fields }}
errorDisplay="none"
onError={(errors) => {
console.log("Validation errors:", errors);
// Custom error handling logic
}}
/>Validation Patterns
Required Fields
const schema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Email is required and must be valid"),
});Optional Fields with Validation
const schema = z.object({
name: z.string().min(1, "Name is required"),
phone: z.string().optional().refine(
(val) => !val || val.length >= 10,
"Phone must be at least 10 digits"
),
});Conditional Validation
const schema = z.object({
hasPhone: z.boolean(),
phone: z.string().optional(),
}).refine(data => {
if (data.hasPhone && !data.phone) {
return false;
}
return true;
}, {
message: "Phone is required when 'Has Phone' is checked",
path: ["phone"],
});Cross-Field Validation
Use Zod’s refine method for validation that depends on multiple fields.
const schema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
startDate: z.date(),
endDate: z.date(),
}).refine(data => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
}).refine(data => data.endDate > data.startDate, {
message: "End date must be after start date",
path: ["endDate"],
});The path option in refine specifies which field should show the error message.
Common Validation Schemas
Hero Hook Form provides helper functions for common validation patterns.
Email Validation
import { createEmailSchema } from "@rachelallyson/hero-hook-form";
const emailSchema = createEmailSchema("Email address");Password Validation
Use createPasswordSchema for consistent password validation across your app.
import { createPasswordSchema } from "@rachelallyson/hero-hook-form";
const passwordSchema = createPasswordSchema({
minLength: 8,
requireUppercase: true,
requireNumbers: true,
requireSpecialChars: true,
});Phone Validation
import { createPhoneSchema } from "@rachelallyson/hero-hook-form";
const phoneSchema = createPhoneSchema("Phone number");URL Validation
import { createUrlSchema } from "@rachelallyson/hero-hook-form";
const urlSchema = createUrlSchema("Website URL");Error Recovery
Retry Logic
const handleSubmit = async (data) => {
let retries = 3;
while (retries > 0) {
try {
await submitToServer(data);
break; // Success
} catch (error) {
retries--;
if (retries === 0) {
// Final failure
setError("submit", { message: "Submission failed after multiple attempts" });
} else {
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
};Backoff Strategy
const handleSubmit = async (data) => {
const backoffDelays = [1000, 2000, 4000]; // Exponential backoff
for (let i = 0; i < backoffDelays.length; i++) {
try {
await submitToServer(data);
break; // Success
} catch (error) {
if (i === backoffDelays.length - 1) {
// Final failure
throw error;
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, backoffDelays[i]));
}
}
};Error States
Form-Level Error State
import { useEnhancedFormState } from "@rachelallyson/hero-hook-form";
function MyForm() {
const formState = useEnhancedFormState();
return (
<div>
{formState.isSubmitting && <div>Submitting...</div>}
{formState.isSubmitted && formState.isSuccess && <div>Success!</div>}
{formState.error && <div>Error: {formState.error}</div>}
</div>
);
}Field-Level Error State
import { useHeroForm } from "@rachelallyson/hero-hook-form";
function MyField({ name }) {
const { formState } = useHeroForm();
const fieldError = formState.errors[name];
return (
<div>
<input name={name} />
{fieldError && <span className="error">{fieldError.message}</span>}
</div>
);
}Custom Error Messages
Field-Specific Messages
const schema = z.object({
email: z.string().email("Please enter a valid email address"),
password: z.string().min(8, "Password must be at least 8 characters long"),
age: z.number().min(18, "You must be at least 18 years old"),
});Contextual Messages
const schema = z.object({
password: z.string().min(8, "Password must be at least 8 characters"),
confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
message: "The passwords you entered don't match. Please try again.",
path: ["confirmPassword"],
});Dynamic Messages
const createPasswordSchema = (minLength) => z.string().min(
minLength,
`Password must be at least ${minLength} characters`
);Testing Error Handling
Testing Validation Errors
import { createFormTestUtils } from "@rachelallyson/hero-hook-form";
const testUtils = createFormTestUtils(form);
// Test required field validation
testUtils.setFieldValue("email", "");
await testUtils.triggerValidation("email");
expect(testUtils.getField("email").error).toBeDefined();
// Test email format validation
testUtils.setFieldValue("email", "invalid-email");
await testUtils.triggerValidation("email");
expect(testUtils.getField("email").error.message).toContain("valid email");Testing Server Errors
// Mock server error response
const mockError = {
fieldErrors: {
email: "Email already exists",
password: "Password is too weak",
},
};
// Apply server errors
applyServerErrors(form, mockError.fieldErrors);
// Verify errors are applied
expect(testUtils.getField("email").error.message).toBe("Email already exists");
expect(testUtils.getField("password").error.message).toBe("Password is too weak");Best Practices
1. Provide Clear Error Messages
// Good: Clear and actionable
"Password must be at least 8 characters and contain at least one number"
// Avoid: Generic or unclear
"Invalid password"2. Validate on Appropriate Events
// Validate on blur for better UX
<InputField
name="email"
label="Email"
inputProps={{
onBlur: () => form.trigger("email"), // Validate on blur
}}
/>3. Handle Network Errors Gracefully
const handleSubmit = async (data) => {
try {
await submitToServer(data);
} catch (error) {
if (error.name === "NetworkError") {
setError("submit", { message: "Network error. Please check your connection." });
} else {
setError("submit", { message: "An unexpected error occurred." });
}
}
};4. Provide Recovery Options
{formState.error && (
<div className="error-container">
<p>{formState.error}</p>
<button onClick={() => form.reset()}>Try Again</button>
</div>
)}5. Test Error Scenarios
// Test all validation rules
it("validates required fields", () => {
// Test required field validation
});
it("validates email format", () => {
// Test email format validation
});
it("handles server errors", () => {
// Test server error handling
});