מאמר זה הוא בחסות 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
return (
);}ראה את ה-Pen SurveyJS-03-RHF [מתפצל] על ידי sixthextinction. יש כאן די הרבה, וכדאי להאט את הקצב כדי לשים לב לאן הדברים הגיעו.
הערכים הנגזרים - סכום משנה, מס, סה"כ - מחושבים ברכיב באמצעות useWatch ו-useMemo מכיוון שהם תלויים בערכי שדות חיים ואין מקום טבעי אחר עבורם. כללי הנראות עבור שם משתמש, סיסמה, משוב חיובי ושיפור משוב פעילים ב-JSX כתנאים מוטבעים. ההיגיון של דילוג הצעדים - דף הסקירה מופיע רק כאשר סך >= 100 - מוטמע במשתנה showSubmit ובתנאי העיבוד בשלב 3. הניווט עצמו הוא רק מונה useState שאנו מגדילים באופן ידני. React Query מטפל בניסיונות חוזרים, שמירה במטמון וביטול תוקף. הטופס פשוט קורא mutation.mutate עם נתונים מאומתים.
כל זה לא פסול, כשלעצמו. זה עדיין React אידיומטי, והרכיב די ביצועי הודות לאופן שבו RHF מבודד עיבוד מחדש. אבל אם היית מוסר את זה למישהו שלא כתב את זה ותבקש ממנו להסביר באילו תנאים מופיע דף הביקורת, הוא יצטרך לעקוב דרך showSubmit, את תנאי העיבוד של שלב 3 והלוגיקה של כפתור הניווט - שלושה מקומות נפרדים - כדי לשחזר כלל שיכול היה להיאמר בשורה אחת. הטופס עובד, כן, אבל ההתנהגות לא ממש ניתנת לבדיקה כמערכת. זה צריך להתבצע נפשית. חשוב מכך, שינוי זה דורש מעורבות הנדסית. אפילו תיקון קטן, כמו התאמה כאשר שלב הבדיקה מופיע, פירושו עריכת הרכיב, עדכון אימות, פתיחת בקשת משיכה, המתנה לבדיקה ופריסה שוב. חלק 2: מונחה סכמה (SurveyJS) עכשיו בואו נבנה את אותה זרימה באמצעות סכמה. התקנה npm להתקין survey-core survey-react-ui @tanstack/react-query
survey-core מנוע זמן ריצה בלתי תלוי בפלטפורמה ברישיון MIT, המניע את עיבוד הטפסים של SurveyJS - החלק שאכפת לנו ממנו כאן. זה לוקח סכימת JSON, בונה ממנה מודל פנימי ומטפל בכל מה שאחרת היה מתגורר ברכיב ה-React שלך: הערכת ביטויי נראות, מחשוב ערכים נגזרים, ניהול מצב עמוד, אימות מעקב והחלטה מה המשמעות של "השלם" בהינתן אילו דפים הוצגו בפועל.
survey-react-ui שכבת ממשק המשתמש / רינדור המחברת את המודל הזה ל-React. זהו בעצם רכיב
יחד, הם נותנים לך זמן ריצה פונקציונלי מלא, מרובה עמודים מבלי לכתוב שורה אחת של זרימת בקרה. פורמט הסכימה עצמו הוא, כאמור, רק JSON - ללא DSL או שום דבר קנייני. אתה יכול לשבץ אותו, לייבא אותו מקובץ, להביא אותו מ-API, או לאחסן אותו בעמודת מסד נתונים ולייבש אותו בזמן ריצה. אותו טופס, כמו נתונים הנה אותה צורה, הפעם מבוטאת כאובייקט JSON. הסכימה מגדירה הכל: מבנה, אימות, כללי נראות, חישובים נגזרים, ניווט בעמודים - ומעבירה אותו למודל שמעריך אותו בזמן ריצה. כך זה נראה במלואו:
export const surveySchema = { title: "זרימת הזמנה", showProgressBar: "top", pages: [ { name: "details", elements: [ { type: "text", name: "firstName", isRequired: true }, { type: "text", name: "email", inputType: "email", isRequired: true, "validers": [{ סוג:"] }, { name: "order", elements: [ { type: "text", name: "price", inputType: "number", defaultValue: 0 }, { type: "text", name: "quantity", inputType: "number", defaultValue: 1 }, { type: "dropdown",name: "taxRate", defaultValue: 0.1, אפשרויות: [ { value: 0.05, text: "5%" }, { value: 0.1, text: "10%" }, { value: 0.15, text: "15%" } ] }, { type: "expression", name: "subtotal" }, type: "subtotal": "expression", name: "tax", expression: "{subtotal} {taxRate}" }, { type: "expression", name: "total", expression: "{subtotal} + {tax}" } ] }, { name: "account", elements: [ { type: "radiogroup", name: "hasAccount", choices: ["]text"",:er" visibleIf: "{hasAccount} = 'כן'", isRequired: true }, { type: "text", name: "password", inputType: "password", visibleIf: "{hasAccount} = 'Yes'", isRequired: true, validators: [{ type: "text", minLength: 6, טקסט: "Min:r" "satisfaction", rateMin: 1, rateMax: 5 }, { type: "comment", name: "positiveFeedback", visibleIf: "{satisfaction} >= 4" }, { type: "comment", name: "improvementFeedback", visibleIf: "{satisfaction} <= 2}" , {}tal visible >= 100", רכיבים: [] } ]};
השווה את זה לגרסת RHF לרגע.
החסימה של superRefine שדרשו על תנאי את שם המשתמש והסיסמה נעלם. visibleIf: "{hasAccount} = 'כן'" בשילוב עם isRequired: true מטפל בשני החששות יחד, בשדה עצמו, שבו היית מצפה למצוא אותם. שרשרת useWatch + useMemo שחישבת סכום ביניים, מס וסה"כ מוחלפת בשלושה שדות ביטוי המפנים זה לזה בשם. מצב דף הסקירה, שבגרסת RHF ניתן היה לשחזר רק על ידי מעקב דרך showSubmit, שלב 3 הענף לעיבוד. ולבסוף, הלוגיקה של כפתור הניווט היא מאפיין visibleIf יחיד באובייקט העמוד.
אותו היגיון קיים. רק שהסכמה נותנת לו מקום לחיות בו הוא נראה במנותק, במקום להתפשט על פני הרכיב. כמו כן, שים לב שהסכימה משתמשת בסוג: 'ביטוי' עבור סיכום ביניים, מס וסה"כ. הביטוי הוא לקריאה בלבד ומשמש בעיקר להצגת ערכים מחושבים. SurveyJS תומך גם בסוג: 'html' עבור תוכן סטטי, אך עבור ערכים מחושבים, ביטוי הוא הבחירה הנכונה. עכשיו לצד ה-React. עיבוד והגשה פשוט מאוד. העבר את onComplete ל-API שלך באותו אופן - באמצעות useMutation או אחזור רגיל:
ייבוא { useState, useEffect, useRef } מ-"react";import { useMutation } מ-"@tanstack/react-query";import { Model } מ-"survey-core";import { Survey } מ-"survey-react-ui";import "survey-core/survey"-survey;
פונקציית ייצוא SurveyForm() { const [model] = useState(() => מודל חדש(surveySchema));
const mutation = useMutation({ mutationFn: אסינכרון (נתונים) => { const res = await fetch("/api/orders", { שיטה: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) throw new Error("נכשל בהגשה"); החזר res.json(); }, });
const mutationRef = useRef(מוטציה); mutationRef.current = מוטציה; useEffect(() => { const handler = (sender) => mutationRef.current.mutate(sender.data); model.onComplete.add(handler); return () => model.onComplete.remove(handler); }, [מודל]); // ref נמנע מרישום מחדש של המטפל בכל עיבוד (זהות אובייקט מוטציה משתנה)
לחזור (
<>
ראה את ה-Pen SurveyJS-03-SurveyJS [מזלג] על ידי sixthextinction.
onComplete מופעל כאשר המשתמש מגיע לסוף העמוד האחרון הגלוי. אז אם סך הכל לעולם לא חוצה 100 ודף הביקורת נדלג, הוא עדיין מופעל בצורה נכונה מכיוון ש-SurveyJS מעריך את הנראות לפני שמחליט מה המשמעות של "דף אחרון". לאחר מכן, sender.data מכיל את כל התשובות יחד עם הערכים המחושבים (סכומי משנה, מס, סה"כ) כשדות ממדרגה ראשונה, כך שמטען ה-API זהה למה שגרסת RHF הרכיבה באופן ידני ב-onSubmit. התבנית mutationRef היא אותה תבנית שאליו תגיע לכל מקום שבו אתה צריך מטפל יציב באירועים על פני ערך שמשתנה בכל עיבוד - שום דבר לא ספציפי ל-SurveyJS בו.
רכיב React כבר אינו מכיל היגיון עסקי כלל. אין useWatch, אין JSX מותנה, אין מונה צעדים, אין שרשרת useMemo, אין superRefine. React עושה את מה שהיא באמת טובה בו: רינדור רכיב וחיווט אותו לקריאת API. מה יצא מ-React?
דאגה מחסנית RHF SurveyJS נראות סניפי JSX visibleIf ערכים נגזרים useWatch / useMemo ביטוי חוקים חוצי שדה superRefine תנאי סכימה ניווט מצב צעד עמוד visibleIf כלל מיקום מופץ על פני קבצים מרוכז בסכמה
מה שנשאר ב-React הוא פריסה, סגנון, חיווט הגשה ושילוב אפליקציות, כלומר, הדברים ש-React מיועדת למעשה עבורם. כל השאר עבר לתוך הסכימה, ומכיוון שהסכימה היא רק אובייקט JSON, ניתן לאחסן אותה במסד נתונים, לגרסאות ללא תלות בקוד האפליקציה שלך, או לערוך אותה באמצעות כלי עבודה פנימיים ללא צורך בפריסה. מנהל מוצר שצריך לשנות את הסף שמפעיל את דף הסקירה יכול לעשות זאת מבלי לגעת ברכיב. זהו הבדל תפעולי משמעותי עבור צוותים שבהם התנהגות צורה מתפתחת לעתים קרובות ולא תמיד מונעת על ידי מהנדסים. מתי להשתמש בכל גישה? הנה כלל אצבע טוב שעובד בשבילי: דמיינו למחוק את הטופס לחלוטין. מה היית מפסיד?
אם זה מסכים, אתה רוצה טפסים מונעי רכיבים. אם זה היגיון עסקי, כמו ספים, חוקי הסתעפות ודרישות מותנות המקודדות החלטות אמיתיות, אתה רוצה מנוע סכימה.
באופן דומה, אם השינויים שמגיעים אליכם נוגעים בעיקר לתוויות, שדות ופריסה, RHF ישרת אתכם היטב. אם הם עוסקים בתנאים, תוצאות וחוקים שאולי המבצעים או הצוות המשפטי שלך צריכים להתאים ביום שלישי אחר הצהריים מבלי להגיש כרטיס, מודל הסכימה עם SurveyJS הוא המתאים יותר. שתי הגישות הללו אינן באמת מתחרות זו בזו. הם מטפלים בסוגים שונים של בעיות, והטעות שכדאי להימנע היא אי התאמה בין ההפשטה למשקל ההיגיון - התייחסות למערכת כללים כאל רכיב כי זה הכלי המוכר, או הגעה למנוע מדיניות כי צורה גדלה לשלושה שלבים ורכשה שדה מותנה. הצורה שבנינו כאן יושבת ליד הגבול בכוונה, מורכבת מספיק כדי לחשוף את ההבדל אבל לא כל כך קיצונית עד שההשוואה מרגישה מזויפת. רוב הטפסים האמיתיים שהפכו למסורבלים בבסיס הקוד שלך כנראה יושבים ליד אותו גבול, והשאלה היא בדרך כלל רק אם מישהו שם מה הם בעצם. השתמש בטופס React Hook + Zod כאשר:
הטפסים מוכווני CRUD; ההיגיון הוא רדוד ומבוסס על ממשק משתמש; מהנדסים הם הבעלים של כל התנהגות; Backend נשאר מקור האמת.
השתמש ב-SurveyJS כאשר:
טפסים מקודדים החלטות עסקיות; כללים מתפתחים ללא תלות בממשק המשתמש; ההיגיון חייב להיות גלוי, ניתן לביקורת או מנוסח; לא מהנדסים משפיעים על התנהגות; אותו טופס חייב לרוץ על פני חזיתות מרובות.