F

Form Libraries

Type-safe form handling with React Hook Form and Zod validation for robust form management.

Code

typescript
1import { useForm, SubmitHandler, Controller } from "react-hook-form";
2import { z } from "zod";
3import { zodResolver } from "@hookform/resolvers/zod";
4
5// Define validation schema with Zod
6const registrationSchema = z.object({
7 username: z
8 .string()
9 .min(3, "Username must be at least 3 characters")
10 .max(20, "Username must be at most 20 characters")
11 .regex(/^[a-zA-Z0-9_]+$/, "Username can only contain letters, numbers, and underscores"),
12 email: z.string().email("Invalid email address"),
13 password: z
14 .string()
15 .min(8, "Password must be at least 8 characters")
16 .regex(/[A-Z]/, "Password must contain at least one uppercase letter")
17 .regex(/[0-9]/, "Password must contain at least one number"),
18 confirmPassword: z.string(),
19 age: z.number().min(18, "Must be at least 18 years old").max(120),
20 role: z.enum(["user", "admin", "moderator"]),
21 newsletter: z.boolean().default(false),
22 website: z.string().url().optional().or(z.literal("")),
23}).refine((data) => data.password === data.confirmPassword, {
24 message: "Passwords don't match",
25 path: ["confirmPassword"],
26});
27
28// Infer TypeScript type from Zod schema
29type RegistrationFormData = z.infer<typeof registrationSchema>;
30
31function RegistrationForm() {
32 const {
33 register,
34 handleSubmit,
35 control,
36 watch,
37 formState: { errors, isSubmitting, isValid },
38 reset,
39 } = useForm<RegistrationFormData>({
40 resolver: zodResolver(registrationSchema),
41 mode: "onChange",
42 defaultValues: {
43 username: "",
44 email: "",
45 password: "",
46 confirmPassword: "",
47 age: 18,
48 role: "user",
49 newsletter: false,
50 website: "",
51 },
52 });
53
54 const onSubmit: SubmitHandler<RegistrationFormData> = async (data) => {
55 try {
56 console.log("Form data:", data);
57 // await registerUser(data);
58 reset();
59 } catch (error) {
60 console.error("Registration failed:", error);
61 }
62 };
63
64 const password = watch("password");
65
66 return (
67 <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
68 {/* Username */}
69 <div>
70 <label htmlFor="username">Username</label>
71 <input
72 id="username"
73 {...register("username")}
74 className={errors.username ? "border-red-500" : ""}
75 />
76 {errors.username && (
77 <span className="text-red-500 text-sm">{errors.username.message}</span>
78 )}
79 </div>
80
81 {/* Email */}
82 <div>
83 <label htmlFor="email">Email</label>
84 <input id="email" type="email" {...register("email")} />
85 {errors.email && (
86 <span className="text-red-500 text-sm">{errors.email.message}</span>
87 )}
88 </div>
89
90 {/* Password */}
91 <div>
92 <label htmlFor="password">Password</label>
93 <input id="password" type="password" {...register("password")} />
94 {errors.password && (
95 <span className="text-red-500 text-sm">{errors.password.message}</span>
96 )}
97 </div>
98
99 {/* Confirm Password */}
100 <div>
101 <label htmlFor="confirmPassword">Confirm Password</label>
102 <input id="confirmPassword" type="password" {...register("confirmPassword")} />
103 {errors.confirmPassword && (
104 <span className="text-red-500 text-sm">{errors.confirmPassword.message}</span>
105 )}
106 </div>
107
108 {/* Age with Controller for number input */}
109 <div>
110 <label htmlFor="age">Age</label>
111 <Controller
112 name="age"
113 control={control}
114 render={({ field }) => (
115 <input
116 id="age"
117 type="number"
118 {...field}
119 onChange={(e) => field.onChange(parseInt(e.target.value, 10))}
120 />
121 )}
122 />
123 {errors.age && <span className="text-red-500 text-sm">{errors.age.message}</span>}
124 </div>
125
126 {/* Role select */}
127 <div>
128 <label htmlFor="role">Role</label>
129 <select id="role" {...register("role")}>
130 <option value="user">User</option>
131 <option value="admin">Admin</option>
132 <option value="moderator">Moderator</option>
133 </select>
134 </div>
135
136 {/* Newsletter checkbox */}
137 <div>
138 <label>
139 <input type="checkbox" {...register("newsletter")} />
140 Subscribe to newsletter
141 </label>
142 </div>
143
144 <button type="submit" disabled={isSubmitting || !isValid}>
145 {isSubmitting ? "Registering..." : "Register"}
146 </button>
147 </form>
148 );
149}
150
151export { RegistrationForm, registrationSchema };

Run this example locally

$ npm install react-hook-form @hookform/resolvers zod