מאמר זה הוא בחסות SurveyJS יש מודל מנטלי שרוב מפתחי React חולקים מבלי לדון בו בקול רם. שצורות תמיד אמורות להיות רכיבים. זה אומר ערימה כמו:

טופס ה-React Hook עבור המדינה המקומית (עיבוד מינימלי מחדש, רישום שדה ארגונומי, אינטראקציה הכרחית). Zod לאימות (נכונות קלט, אימות גבול, ניתוח מסוג בטוח). React Query עבור backend: הגשה, ניסיונות חוזרים, שמירה במטמון, סנכרון שרת, וכן הלאה.

ועבור הרוב המכריע של הטפסים - מסכי הכניסה שלך, דפי ההגדרות שלך, מודלי ה-CRUD שלך - זה עובד ממש טוב. כל חלק עושה את העבודה שלו, הם מרכיבים בצורה נקייה, ואתה יכול לעבור לחלקים של האפליקציה שלך שבעצם מבדילים את המוצר שלך. אבל מדי פעם, טופס מתחיל לצבור דברים כמו כללי נראות התלויים בתשובות קודמות, או ערכים נגזרים שמתגלגלים דרך שלושה שדות. אולי אפילו עמודים שלמים שצריך לדלג עליהם או להציג אותם על סמך סך הכל. אתה מטפל בתנאי הראשון עם useWatch וענף מוטבע, וזה בסדר. ואז עוד אחד. אז אתה מגיע ל- superRefine כדי לקודד חוקים חוצי שדות שסכימת Zod שלך לא יכולה לבטא בדרך הרגילה. לאחר מכן, ניווט שלב מתחיל להדליף היגיון עסקי. בשלב מסוים, אתה מסתכל על מה שבנית ומבין שהטופס כבר לא ממש ממשק משתמש. זה יותר תהליך החלטה, ועץ הרכיבים הוא בדיוק המקום שבו במקרה אחסנת אותו. זה המקום שבו אני חושב שהמודל המנטלי לטפסים ב-React מתקלקל, וזו באמת לא אשמתו של אף אחד. ערימת RHF + Zod מצוינת במה שהיא תוכננה עבורו. הבעיה היא שאנו נוטים להמשיך להשתמש בו מעבר לנקודה שבה ההפשטות שלו תואמות את הבעיה, כי האלטרנטיבה דורשת דרך אחרת לחשוב על צורות לחלוטין. מאמר זה עוסק בחלופה זו. כדי להראות זאת, נבנה את אותו טופס רב-שלבי בדיוק פעמיים:

עם טופס React Hook + Zod מחוברים ל-React Query להגשה, עם SurveyJS, שמתייחס לטופס כאל נתונים - סכימת JSON פשוטה - ולא כעץ רכיבים.

אותן דרישות, אותה היגיון מותנה, אותה קריאת API בסוף. לאחר מכן נמפה בדיוק מה זז ומה נשאר, ונפרט דרך מעשית להחליט באיזה דגם כדאי להשתמש ומתי. הטופס שאנו בונים:

טופס זה ישתמש בזרימה בת 4 שלבים: שלב 1: פרטים

שם פרטי (חובה), דואר אלקטרוני (חובה, פורמט חוקי).

שלב 2: הזמנה

מחיר ליחידה, כמות, שיעור מס, נגזר: סה"כ, מס, סך הכל.

שלב 3: חשבון ומשוב

יש לך חשבון? (כן/לא) אם כן → שם משתמש + סיסמה, שניהם נדרשים. אם לא → דוא"ל כבר נאסף בשלב 1.

דירוג שביעות רצון (1-5) אם ≥ 4 → תשאל "מה אהבת?" אם ≤ 2 → לשאול "מה אנחנו יכולים לשפר?"

שלב 4: סקירה

מופיע רק אם סך >= 100 הגשה סופית.

זה לא קיצוני. אבל זה מספיק כדי לחשוף הבדלים אדריכליים. חלק 1: מונע רכיבים (טופס הוק תגובה + זוד) התקנה npm להתקין react-hook-form zod @hookform/resolvers @tanstack/react-query

Zod Schema נתחיל עם סכימת Zod, כי זה בדרך כלל המקום שבו הצורה של הטופס מתבססת. עבור שני השלבים הראשונים - פרטים אישיים וקלט הזמנה - הכל פשוט: מחרוזות נדרשות, מספרים עם מינימום, ומנה. החלק המעניין מתחיל כשמנסים לבטא את הכללים המותנים.

ייבוא ​​{ z } מ-"zod";

export const formSchema = z.object({ firstName: z.string().min(1, "Required"), אימייל: z.string().email("אימייל לא חוקי"), מחיר: z.number().min(0), כמות: z.number().min(1), taxRate: z.number(), hasAccount: z.enum(No["Yes), hasAccount: z.enum(No["Y.es), user "Noz"(Y),(op). סיסמה: z.string().optional(), שביעות רצון: z.number().min(1).max(5), משוב חיובי: z.string().optional(), שיפור משוב: z.string().optional(),}).superRefine((data, ctx) => { if (data.hasAccount ===) {.data") ctx.addIssue({ קוד: "מותאם אישית", נתיב: ["שם משתמש"], הודעה: "נדרשת" } } if (!data.password || data.password.length < 6) { ctx.addIssue({ קוד: "מותאם אישית", נתיב: ["סיסמה"], הודעה: "מינימום 6 תווים"});

if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ קוד: "מותאם אישית", נתיב: ["משוב חיובי"], הודעה: "אנא שתף את מה שאהבת" }); }

if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ code: "custom", path:["משוב לשיפור"], הודעה: "אנא ספר לנו מה לשפר" }); }});

סוג ייצוא FormData = z.infer;

שים לב ששם המשתמש והסיסמה מוקלדים כאופציונליים () למרות שהם נדרשים באופן מותנה מכיוון שהסכימה ברמת הסוג של Zod מתארת ​​את צורת האובייקט, ולא את הכללים שקובעים מתי שדות חשובים. הדרישה המותנית צריכה לחיות בתוך superRefine, שפועל לאחר אימות הצורה ויש לו גישה לאובייקט המלא. ההפרדה הזו אינה פגם; בדיוק בשביל זה תוכנן הכלי: superRefine הוא המקום שבו הלוגיקה בין שדה עוברת כאשר לא ניתן לבטא אותה במבנה הסכמה עצמו. מה שגם בולט כאן הוא מה הסכימה הזו לא מבטאת. אין לו מושג של דפים, אין מושג של אילו שדות נראים באיזו נקודה, ואין מושג של ניווט. כל זה יחיה במקום אחר. רכיב טופס

יבוא { useForm, useWatch } מ-"react-hook-form"; יבוא { zodResolver } מ-"@hookform/resolvers/zod"; יבוא { useMutation } מ-"@tanstack/react-query";import { useState, useMemo } מ-"react";import {formaSchema" מ-"Form.DataSchema};

const STEPS = ["פרטים", "הזמנה", "חשבון", "סקירה"];

type OrderPayload = FormData & { subtotal: number; מס: מספר; סך: מספר };

פונקציית ייצוא RHFMultiStepForm() { const [step, setStep] = useState(0);

const mutation = useMutation({ mutationFn: אסינכרון (עומס: OrderPayload) => { const res = await fetch("/api/orders", { שיטה: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error("נכשל בהגשה"); החזר res.json(); }, });

const { register, control, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(formSchema), defaultValues: { price: 0, quantity: 1, taxRate: 0.1, satisfaction: 3, hasAccount: "No", }, }); const price = useWatch({ control, name: "price" }); const quantity = useWatch({ control, name: "quantity" }); const taxRate = useWatch({ control, name: "taxRate" }); const hasAccount = useWatch({ control, name: "hasAccount" }); const satisfaction = useWatch({ control, name: "satisfaction" }); const subtotal = useMemo(() => (מחיר ?? 0) * (כמות ?? 1), [מחיר, כמות]); const tax = useMemo(() => subtotal * (taxRate ?? 0), [subtotal, taxRate]); const total = useMemo(() => סכומי משנה + מס, [סכומי משנה, מס]); const onSubmit = (נתונים: FormData) => mutation.mutate({ ...data, subtotal, tax, total }); const showSubmit = (שלב === 2 && סך הכל < 100) || (שלב === 3 && סך הכל >= 100)

return (

{שלב === 0 && ( <> )}

{step === 1 && ( <>

סך משנה: {subtotal}
מס: {tax}
סה"כ: {total}
)}

{step === 2 && ( <> <בחר {...register("hasAccount")}>

{hasAccount === "כן" && ( <> )}

{סיפוק >= 4 && (