7.9 טפסים React Hook Form ו Zod הרצאה
טפסים - React Hook Form ו-Zod¶
בשיעור הזה נלמד על React Hook Form - ספריית ניהול טפסים עם ביצועים גבוהים, ועל Zod - ספריית ולידציה עם TypeScript. נלמד איך לשלב ביניהן לטפסים מורכבים עם ולידציה חזקה.
למה ספריית טפסים?¶
בעיות עם טפסים רגילים בריאקט¶
// הגישה הנאיבית
function ContactForm() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [message, setMessage] = useState("");
const [errors, setErrors] = useState({});
// כל שינוי בשדה גורם לרנדר של כל הטופס!
// ולידציה ידנית - הרבה קוד שחוזר על עצמו
}
בעיות:
- כל שינוי בשדה גורם לרנדר מחדש של כל הטופס (בעיית ביצועים)
- ולידציה ידנית דורשת הרבה קוד
- ניהול state של שדות רבים מסורבל
- ניהול שגיאות ו-touched state מורכב
התקנה¶
React Hook Form - בסיס¶
שימוש בסיסי¶
import { useForm } from "react-hook-form";
interface FormData {
name: string;
email: string;
message: string;
}
function ContactForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>();
const onSubmit = async (data: FormData) => {
console.log("נתונים:", data);
await new Promise((r) => setTimeout(r, 1000)); // סימולציה
alert("הטופס נשלח!");
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>שם</label>
<input
{...register("name", {
required: "שם הוא שדה חובה",
minLength: { value: 2, message: "לפחות 2 תווים" },
})}
/>
{errors.name && <p style={{ color: "red" }}>{errors.name.message}</p>}
</div>
<div>
<label>אימייל</label>
<input
type="email"
{...register("email", {
required: "אימייל הוא שדה חובה",
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: "אימייל לא תקין",
},
})}
/>
{errors.email && <p style={{ color: "red" }}>{errors.email.message}</p>}
</div>
<div>
<label>הודעה</label>
<textarea
{...register("message", {
required: "הודעה היא שדה חובה",
maxLength: { value: 500, message: "מקסימום 500 תווים" },
})}
/>
{errors.message && <p style={{ color: "red" }}>{errors.message.message}</p>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "שולח..." : "שלח"}
</button>
</form>
);
}
registerמחבר שדה input לטופס (מחזיר ref, onChange, onBlur, name)handleSubmitעוטף את פונקציית ה-submit ומריץ ולידציה לפניerrorsמכיל שגיאות ולידציה לכל שדה- הטופס לא גורם לרנדר מחדש בכל שינוי שדה - רק כש-submit או כשיש שגיאה
אפשרויות useForm¶
const form = useForm<FormData>({
defaultValues: {
name: "",
email: "",
message: "",
},
mode: "onBlur", // מתי לבדוק ולידציה: onChange, onBlur, onSubmit, onTouched, all
});
ולידציה עם Zod¶
מה זה Zod?¶
Zod היא ספריית ולידציה ל-TypeScript שמאפשרת להגדיר סכמות (schemas) ולבצע ולידציה עם טיפוסים אוטומטיים:
import { z } from "zod";
// הגדרת סכמה
const userSchema = z.object({
name: z
.string()
.min(2, "שם חייב להכיל לפחות 2 תווים")
.max(50, "שם ארוך מדי"),
email: z
.string()
.email("כתובת אימייל לא תקינה"),
age: z
.number()
.min(18, "גיל מינימלי 18")
.max(120, "גיל לא תקין"),
website: z
.string()
.url("כתובת URL לא תקינה")
.optional(),
});
// הטיפוס נגזר אוטומטית מהסכמה
type User = z.infer<typeof userSchema>;
// User = { name: string; email: string; age: number; website?: string }
סוגי ולידציה ב-Zod¶
// מחרוזות
z.string().min(1).max(100).email().url().startsWith("https://")
// מספרים
z.number().int().positive().min(0).max(100)
// בוליאני
z.boolean()
// תאריכים
z.date().min(new Date("2020-01-01"))
// Enum
z.enum(["admin", "user", "guest"])
// מערך
z.array(z.string()).min(1).max(10)
// אובייקט מקונן
z.object({
address: z.object({
street: z.string(),
city: z.string(),
}),
})
// Union
z.union([z.string(), z.number()])
// ולידציה מותאמת
z.string().refine((val) => val.includes("@"), "חייב לכלול @")
// טרנספורמציה
z.string().transform((val) => val.trim().toLowerCase())
שילוב React Hook Form עם Zod¶
zodResolver¶
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const registrationSchema = z
.object({
name: z.string().min(2, "שם חייב להכיל לפחות 2 תווים"),
email: z.string().email("אימייל לא תקין"),
password: z
.string()
.min(8, "סיסמה חייבת להכיל לפחות 8 תווים")
.regex(/[A-Z]/, "חייבת לכלול אות גדולה")
.regex(/[0-9]/, "חייבת לכלול ספרה"),
confirmPassword: z.string(),
terms: z.boolean().refine((val) => val, "יש לאשר את התנאים"),
})
.refine((data) => data.password === data.confirmPassword, {
message: "הסיסמאות לא תואמות",
path: ["confirmPassword"],
});
type RegistrationData = z.infer<typeof registrationSchema>;
function RegistrationForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<RegistrationData>({
resolver: zodResolver(registrationSchema),
defaultValues: {
name: "",
email: "",
password: "",
confirmPassword: "",
terms: false,
},
});
const onSubmit = async (data: RegistrationData) => {
console.log("נתונים תקינים:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>שם</label>
<input {...register("name")} />
{errors.name && <p style={{ color: "red" }}>{errors.name.message}</p>}
</div>
<div>
<label>אימייל</label>
<input type="email" {...register("email")} />
{errors.email && <p style={{ color: "red" }}>{errors.email.message}</p>}
</div>
<div>
<label>סיסמה</label>
<input type="password" {...register("password")} />
{errors.password && (
<p style={{ color: "red" }}>{errors.password.message}</p>
)}
</div>
<div>
<label>אישור סיסמה</label>
<input type="password" {...register("confirmPassword")} />
{errors.confirmPassword && (
<p style={{ color: "red" }}>{errors.confirmPassword.message}</p>
)}
</div>
<div>
<label>
<input type="checkbox" {...register("terms")} />
אני מאשר את התנאים
</label>
{errors.terms && <p style={{ color: "red" }}>{errors.terms.message}</p>}
</div>
<button type="submit" disabled={isSubmitting}>
הירשם
</button>
</form>
);
}
zodResolverמחבר את סכמת Zod ל-React Hook Form- הטיפוס נגזר אוטומטית מהסכמה - אין צורך להגדיר interface נפרד
refineברמת הסכמה מאפשר ולידציה שתלויה במספר שדות (כמו אישור סיסמה)
טפסים מורכבים - מערכים¶
import { useForm, useFieldArray } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const orderSchema = z.object({
customerName: z.string().min(2),
items: z
.array(
z.object({
name: z.string().min(1, "שם מוצר נדרש"),
quantity: z.number().min(1, "כמות מינימלית 1"),
price: z.number().min(0, "מחיר לא תקין"),
})
)
.min(1, "יש להוסיף לפחות מוצר אחד"),
});
type OrderData = z.infer<typeof orderSchema>;
function OrderForm() {
const {
register,
control,
handleSubmit,
formState: { errors },
} = useForm<OrderData>({
resolver: zodResolver(orderSchema),
defaultValues: {
customerName: "",
items: [{ name: "", quantity: 1, price: 0 }],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: "items",
});
const onSubmit = (data: OrderData) => {
console.log("הזמנה:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>שם לקוח</label>
<input {...register("customerName")} />
{errors.customerName && (
<p style={{ color: "red" }}>{errors.customerName.message}</p>
)}
</div>
<h3>מוצרים</h3>
{fields.map((field, index) => (
<div key={field.id} style={{ border: "1px solid #ddd", padding: "12px", margin: "8px 0" }}>
<input
{...register(`items.${index}.name`)}
placeholder="שם מוצר"
/>
{errors.items?.[index]?.name && (
<p style={{ color: "red" }}>{errors.items[index]?.name?.message}</p>
)}
<input
type="number"
{...register(`items.${index}.quantity`, { valueAsNumber: true })}
placeholder="כמות"
/>
<input
type="number"
{...register(`items.${index}.price`, { valueAsNumber: true })}
placeholder="מחיר"
/>
<button type="button" onClick={() => remove(index)}>
הסר
</button>
</div>
))}
{errors.items?.root && (
<p style={{ color: "red" }}>{errors.items.root.message}</p>
)}
<button type="button" onClick={() => append({ name: "", quantity: 1, price: 0 })}>
הוסף מוצר
</button>
<button type="submit">שלח הזמנה</button>
</form>
);
}
useFieldArrayמנהל מערך דינמי של שדותappend,remove,insert,move- פעולות על המערךvalueAsNumberממיר את הערך למספר אוטומטית
אובייקטים מקוננים¶
const addressSchema = z.object({
personal: z.object({
firstName: z.string().min(1, "שם פרטי נדרש"),
lastName: z.string().min(1, "שם משפחה נדרש"),
}),
address: z.object({
street: z.string().min(1, "רחוב נדרש"),
city: z.string().min(1, "עיר נדרשת"),
zip: z.string().regex(/^\d{7}$/, "מיקוד חייב להכיל 7 ספרות"),
}),
});
type AddressData = z.infer<typeof addressSchema>;
function AddressForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<AddressData>({
resolver: zodResolver(addressSchema),
});
return (
<form onSubmit={handleSubmit(console.log)}>
<input {...register("personal.firstName")} placeholder="שם פרטי" />
{errors.personal?.firstName && (
<p style={{ color: "red" }}>{errors.personal.firstName.message}</p>
)}
<input {...register("personal.lastName")} placeholder="שם משפחה" />
<input {...register("address.street")} placeholder="רחוב" />
<input {...register("address.city")} placeholder="עיר" />
<input {...register("address.zip")} placeholder="מיקוד" />
{errors.address?.zip && (
<p style={{ color: "red" }}>{errors.address.zip.message}</p>
)}
<button type="submit">שלח</button>
</form>
);
}
הודעות שגיאה ו-UX¶
קומפוננטת שגיאה¶
interface FormFieldProps {
label: string;
error?: string;
children: React.ReactNode;
}
function FormField({ label, error, children }: FormFieldProps) {
return (
<div style={{ marginBottom: "16px" }}>
<label style={{ display: "block", marginBottom: "4px", fontWeight: "bold" }}>
{label}
</label>
{children}
{error && (
<p
style={{
color: "#dc3545",
fontSize: "13px",
marginTop: "4px",
}}
>
{error}
</p>
)}
</div>
);
}
// שימוש
function MyForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<FormField label="שם" error={errors.name?.message}>
<input {...register("name")} />
</FormField>
<FormField label="אימייל" error={errors.email?.message}>
<input type="email" {...register("email")} />
</FormField>
</form>
);
}
Watch - מעקב אחרי ערכים¶
function DynamicForm() {
const { register, watch } = useForm();
const accountType = watch("accountType");
return (
<form>
<select {...register("accountType")}>
<option value="personal">אישי</option>
<option value="business">עסקי</option>
</select>
{accountType === "business" && (
<div>
<input {...register("companyName")} placeholder="שם חברה" />
<input {...register("taxId")} placeholder="מספר עוסק" />
</div>
)}
</form>
);
}
Reset¶
function ResettableForm() {
const { register, handleSubmit, reset } = useForm({
defaultValues: { name: "", email: "" },
});
const onSubmit = (data: any) => {
console.log(data);
reset(); // חזרה לערכי ברירת מחדל
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name")} />
<input {...register("email")} />
<button type="submit">שלח</button>
<button type="button" onClick={() => reset()}>
נקה
</button>
</form>
);
}
סיכום¶
- React Hook Form מנהלת טפסים עם ביצועים גבוהים - לא גורמת לרנדר מחדש בכל שינוי שדה
- register מחבר שדה input לטופס, handleSubmit מטפל בשליחה עם ולידציה
- Zod מאפשרת להגדיר סכמות ולידציה עם טיפוסי TypeScript אוטומטיים
- zodResolver מחבר בין Zod ל-React Hook Form
- useFieldArray מטפל במערכים דינמיים של שדות
- גישה מקוננת (nested objects) עובדת עם dot notation (למשל "address.city")
- watch מאפשר לעקוב אחרי ערכי שדות ולהציג תוכן דינמי
- reset מאפס את הטופס לערכי ברירת מחדל