هذه المقالة برعاية SurveyJS هناك نموذج عقلي يشاركه معظم مطوري React دون مناقشته بصوت عالٍ. من المفترض دائمًا أن تكون هذه النماذج مكونات. وهذا يعني مكدس مثل:
React Hook Form للحالة المحلية (الحد الأدنى من عمليات إعادة العرض، وتسجيل المجال المريح، والتفاعل الحتمي). Zod للتحقق من الصحة (صحة الإدخال، والتحقق من صحة الحدود، والتحليل الآمن للنوع). React Query للواجهة الخلفية: الإرسال، وإعادة المحاولة، والتخزين المؤقت، ومزامنة الخادم، وما إلى ذلك.
وبالنسبة للغالبية العظمى من النماذج - شاشات تسجيل الدخول، وصفحات الإعدادات، ونماذج CRUD الخاصة بك - فإن هذا يعمل بشكل جيد حقًا. تقوم كل قطعة بعملها، ويتم تركيبها بشكل نظيف، ويمكنك الانتقال إلى أجزاء تطبيقك التي تميز منتجك فعليًا. ولكن من حين لآخر، يبدأ النموذج في تجميع أشياء مثل قواعد الرؤية التي تعتمد على الإجابات السابقة، أو القيم المشتقة التي تتسلسل عبر ثلاثة حقول. ربما حتى صفحات كاملة يجب تخطيها أو عرضها بناءً على الإجمالي الجاري. يمكنك التعامل مع الشرط الأول باستخدام useWatch والفرع المضمّن، وهو أمر جيد. ثم آخر. ثم إنك تصل إلى superRefine لتشفير القواعد عبر الحقول التي لا يستطيع مخطط Zod الخاص بك التعبير عنها بالطريقة العادية. وبعد ذلك، يبدأ التنقل بين الخطوات في تسريب منطق الأعمال. في مرحلة ما، تنظر إلى ما قمت بإنشائه وتدرك أن النموذج لم يعد واجهة مستخدم حقًا بعد الآن. إنها أكثر من مجرد عملية اتخاذ قرار، وشجرة المكونات هي المكان الذي قمت بتخزينها فيه. أعتقد أن هذا هو المكان الذي ينهار فيه النموذج العقلي للنماذج في React، وهذا في الحقيقة خطأ لا أحد. يعتبر مكدس RHF + Zod ممتازًا فيما تم تصميمه من أجله. تكمن المشكلة في أننا نميل إلى الاستمرار في استخدامه بعد النقطة التي تتطابق فيها تجريداته مع المشكلة لأن البديل يتطلب طريقة مختلفة للتفكير في النماذج تمامًا. هذه المقالة هي عن هذا البديل. لإظهار ذلك، سنقوم ببناء نفس النموذج متعدد الخطوات مرتين:
باستخدام React Hook Form + Zod المتصل باستعلام React لتقديمه، باستخدام SurveyJS، الذي يتعامل مع النموذج كبيانات - مخطط JSON بسيط - بدلاً من شجرة المكونات.
نفس المتطلبات، نفس المنطق الشرطي، نفس استدعاء API في النهاية. بعد ذلك، سنرسم خريطة دقيقة لما تم نقله وما بقي، وسنضع طريقة عملية لتحديد النموذج الذي يجب عليك استخدامه ومتى. النموذج الذي نقوم ببنائه:
سيستخدم هذا النموذج تدفقًا من 4 خطوات: الخطوة 1: التفاصيل
الاسم الأول (مطلوب)، البريد الإلكتروني (مطلوب، تنسيق صالح).
الخطوة 2: النظام
سعر الوحدة، الكمية, معدل الضريبة، مشتق: المجموع الفرعي، الضريبة، المجموع.
الخطوة 3: الحساب والتعليقات
هل لديك حساب؟ (نعم/لا) إذا كانت الإجابة بنعم → اسم المستخدم + كلمة المرور، فكلاهما مطلوب. إذا كانت الإجابة لا → البريد الإلكتروني الذي تم جمعه بالفعل في الخطوة 1.
تقييم الرضا (1-5) إذا ≥ 4 ← اسأل "ما الذي أعجبك؟" إذا ≥ 2 ← اسأل "ما الذي يمكننا تحسينه؟"
الخطوة 4: المراجعة
يظهر فقط إذا كان الإجمالي >= 100 التقديم النهائي.
هذا ليس متطرفا. لكن هذا يكفي لكشف الاختلافات المعمارية. الجزء 1: يعتمد على المكونات (نموذج React Hook + Zod) التثبيت npm تثبيت رد فعل هوك على شكل zod @hookform/resolvers @tanstack/react-query
مخطط زود لنبدأ بمخطط Zod، لأنه عادةً ما يتم تحديد شكل النموذج. بالنسبة للخطوتين الأوليين - التفاصيل الشخصية ومدخلات الطلب - كل شيء واضح ومباشر: السلاسل المطلوبة، والأرقام ذات الحد الأدنى، والتعداد. يبدأ الجزء المثير للاهتمام عندما تحاول التعبير عن القواعد الشرطية.
استيراد {ض} من "zod"؛
تصدير constformSchema = z.object({ firstName: z.string().min(1, "Required"), البريد الإلكتروني: z.string().email("بريد إلكتروني غير صالح"), السعر: z.number().min(0), الكمية: z.number().min(1), معدل الضريبة: z.number(), hasAccount: z.enum(["Yes", "No"]), اسم المستخدم: z.string().اختياري(), كلمة المرور: z.string().اختياري()، الرضا: z.number().min(1).max(5)، ردود فعل إيجابية: z.string().اختياري()، تحسين ردود الفعل: z.string().اختياري()،}).superRefine((data, ctx) => { if (data.hasAccount === "Yes") { if (!data.username) { ctx.addIssue({ code: "custom"، path: ["اسم المستخدم"]، الرسالة: "مطلوب" }); } if (!data.password || data.password.length < 6) { ctx.addIssue({ code: "custom"، path: ["password"]، الرسالة: "6 أحرف على الأقل" } });
if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ code: "custom"، المسار: ["positiveFeedback"]، الرسالة: "الرجاء مشاركة ما أعجبك" }); }
إذا (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ code: "custom"، المسار:["improvementFeedback"]، message: "من فضلك أخبرنا بما يجب تحسينه" }); }});
نوع التصدير FormData = z.infer
لاحظ أنه يتم كتابة اسم المستخدم وكلمة المرور كاختياريين () على الرغم من أنهما مطلوبان بشكل مشروط لأن مخطط مستوى النوع في Zod يصف شكل الكائن، وليس القواعد التي تحكم متى تكون الحقول مهمة. يجب أن يعيش المتطلب الشرطي داخل superRefine، والذي يعمل بعد التحقق من صحة الشكل ولديه حق الوصول إلى الكائن الكامل. وهذا الانفصال ليس عيبا. إنه فقط ما تم تصميم الأداة من أجله: superRefine هو المكان الذي يذهب إليه المنطق عبر الحقول عندما لا يمكن التعبير عنه في بنية المخطط نفسه. ما هو ملحوظ هنا أيضًا هو ما لا يعبر عنه هذا المخطط. ليس لديها مفهوم للصفحات، ولا يوجد مفهوم للحقول التي يمكن رؤيتها عند أي نقطة، ولا يوجد مفهوم للتنقل. كل ذلك سيعيش في مكان آخر. مكون النموذج
استيراد { useForm, useWatch } من "react-hook-form";استيراد { zodResolver } من "@hookform/resolvers/zod";استيراد { useMutation } من "@tanstack/react-query";استيراد { useState, useMemo } من "react";استيراد {formSchema, اكتب FormData } من "./schema";
const STEPS = ["التفاصيل"، "الطلب"، "الحساب"، "المراجعة"]؛
اكتب OrderPayload = FormData & { المجموع الفرعي: الرقم؛ الضريبة: رقم؛ الإجمالي: العدد };
وظيفة التصدير RHFMultiStepForm() { const [step, setStep] = useState(0);
طفرة ثابتة = useMutation({ MutationFn: غير متزامن (الحمولة: OrderPayload) => { const res = انتظار الجلب("/api/orders"، { الطريقة: "POST"، الرؤوس: { "نوع المحتوى": "application/json" }، الجسم: JSON.stringify (الحمولة)، }); إذا (!res.ok) ألقى خطأ جديد("فشل الإرسال"); إرجاع res.json(); }, });
const { Register, control, HandleSubmit,formState: { الأخطاء }, } = useForm
return (
);}راجع Pen SurveyJS-03-RHF [متشعب] بواسطة sixextinction. هناك الكثير مما يحدث هنا، ويجدر بنا أن نتباطأ لنلاحظ أين انتهت الأمور.
يتم حساب القيم المشتقة - الإجمالي الفرعي والضريبة والإجمالي - في المكون عبر useWatch وuseMemo لأنها تعتمد على قيم الحقول المباشرة ولا يوجد مكان طبيعي آخر لها. قواعد الرؤية لاسم المستخدم وكلمة المرور والتعليقات الإيجابية وملاحظات التحسين موجودة في JSX كشروط مضمنة. يتم تضمين منطق تخطي الخطوات - تظهر صفحة المراجعة فقط عندما يكون الإجمالي >= 100 - في متغير showSubmit وشرط العرض في الخطوة 3. التنقل بحد ذاته هو مجرد عداد useState الذي نقوم بزيادته يدويًا. يعالج React Query عمليات إعادة المحاولة والتخزين المؤقت والإبطال. يستدعي النموذج فقط mutate.mutate مع البيانات التي تم التحقق من صحتها.
لا شيء من هذا خطأ في حد ذاته. لا يزال هذا React اصطلاحيًا، والمكون ذو أداء جيد جدًا بفضل كيفية عزل RHF لإعادة التصيير. ولكن إذا قمت بتسليم هذا إلى شخص لم يكتبه بعد وطلبت منه أن يشرح تحت أي ظروف تظهر صفحة المراجعة، فسيتعين عليه التتبع من خلال showSubmit، وشرط العرض في الخطوة 3، ومنطق زر التنقل - ثلاثة أماكن منفصلة - لإعادة بناء قاعدة يمكن ذكرها في سطر واحد. النموذج يعمل، نعم، ولكن السلوك لا يمكن فحصه حقًا كنظام. يجب أن يتم تنفيذه عقليا. والأهم من ذلك أن تغييره يتطلب مشاركة هندسية. حتى التعديل الصغير، مثل التعديل عند ظهور خطوة المراجعة، يعني تحرير المكون، وتحديث التحقق من الصحة، وفتح طلب سحب، وانتظار المراجعة، والنشر مرة أخرى. الجزء 2: يحركها المخطط (SurveyJS) الآن دعونا نبني نفس التدفق باستخدام المخطط. التثبيت npm تثبيت المسح الأساسي للمسح-react-ui @tanstack/react-query
Survey-core محرك وقت التشغيل المستقل عن النظام الأساسي المرخص من MIT والذي يعمل على تشغيل عرض نماذج SurveyJS - وهو الجزء الذي نهتم به هنا. فهو يأخذ مخطط JSON، ويبني نموذجًا داخليًا منه، ويتعامل مع كل ما يمكن أن يوجد في مكون React الخاص بك: تقييم تعبيرات الرؤية، وحساب القيم المشتقة، وإدارة حالة الصفحة، وتتبع التحقق من الصحة، وتحديد معنى "مكتمل" بالنظر إلى الصفحات التي تم عرضها بالفعل.
Survey-react-uiطبقة واجهة المستخدم/العرض التي تربط هذا النموذج بـ React. إنه في الأساس مكون
معًا، يوفران لك وقت تشغيل نموذج متعدد الصفحات يعمل بكامل طاقته دون كتابة سطر واحد من تدفق التحكم. تنسيق المخطط نفسه، كما ذكرنا من قبل، هو مجرد JSON - لا يوجد DSL أو أي شيء خاص. يمكنك تضمينه أو استيراده من ملف أو جلبه من واجهة برمجة التطبيقات (API) أو تخزينه في عمود قاعدة بيانات وترطيبه في وقت التشغيل. نفس النموذج، مثل البيانات إليك نفس النموذج، معبرًا عنه هذه المرة ككائن JSON. يحدد المخطط كل شيء: البنية، والتحقق من الصحة، وقواعد الرؤية، والحسابات المشتقة، والتنقل في الصفحة - ويسلمها إلى نموذج يقوم بتقييمها في وقت التشغيل. إليك ما يبدو عليه الأمر بالكامل:
Export const SurveySchema = { title: "Order Flow"، showProgressBar: "top"، الصفحات: [ { name: "details"، العناصر: [ { type: "text"، name: "firstName"، isRequired: true }، { type: "text"، name: "email"، inputType: "email"، isRequired: true، validators: [{ type: "email"، text: "invalid email" }] } ] }, { الاسم: "طلب"، العناصر: [ { النوع: "نص"، الاسم: "السعر"، نوع الإدخال: "رقم"، القيمة الافتراضية: 0 }، { النوع: "نص"، الاسم: "الكمية"، نوع الإدخال: "رقم"، القيمة الافتراضية: 1 }، { النوع: "قائمة منسدلة"،الاسم: "taxRate"، القيمة الافتراضية: 0.1، الاختيارات: [ { value: 0.05، text: "5%" }، { value: 0.1، text: "10%" }، { value: 0.15، text: "15%" } ] }، { type: "expression"، الاسم: "subtotal"، التعبير: "{price} {quantity}" }، { type: "تعبير"، الاسم: "ضريبة"، التعبير: "{المجموع الفرعي} {taxRate}" }، { النوع: "التعبير"، الاسم: "الإجمالي"، التعبير: "{المجموع الفرعي} + {الضريبة}" } ] }، { الاسم: "الحساب"، العناصر: [ { النوع: "radiogroup"، الاسم: "hasAccount"، الاختيارات: ["نعم"، "لا"] }، { النوع: "نص"، الاسم: "اسم المستخدم"، visualIf: "{hasAccount} = 'Yes'"، مطلوب: صحيح }، { النوع: "نص"، الاسم: "كلمة المرور"، نوع الإدخال: "كلمة المرور"، visualIf: "{hasAccount} = 'نعم'"، مطلوب: صحيح، أدوات التحقق من الصحة: [{ type: "text"، الحد الأدنى للطول: 6، النص: "6 أحرف على الأقل" }] }، { النوع: "تصنيف"، الاسم: "satisfaction"، RateMin: 1، RateMax: 5 }، { type: "comment"، name: "positiveFeedback"، visualIf: "{satisfaction} >= 4" }، { type: "comment"، name: "improvementFeedback"، visualIf: "{satisfaction} <= 2" } ] }، { name: "review"، visualIf: "{total} >= 100"، العناصر: [] } ]}؛
قارن هذا بإصدار RHF للحظة.
لقد اختفت كتلة superRefine التي كانت تتطلب اسم المستخدم وكلمة المرور بشكل مشروط. visualIf: "{hasAccount} = 'Yes'" مدمج مع isRequired: true يعالج كلا الاهتمامين معًا، في الحقل نفسه، حيث تتوقع العثور عليهما. يتم استبدال سلسلة useWatch + useMemo التي تحسب الإجمالي الفرعي والضريبة والإجمالي بثلاثة حقول تعبير تشير إلى بعضها البعض بالاسم. حالة صفحة المراجعة، والتي كانت قابلة لإعادة البناء في إصدار RHF فقط من خلال التتبع من خلال showSubmit، فرع العرض في الخطوة 3. وأخيرًا، منطق زر التنقل هو خاصية مرئية واحدة في كائن الصفحة.
نفس المنطق موجود. كل ما في الأمر هو أن المخطط يمنحه مكانًا للعيش حيث يكون مرئيًا بمعزل عن الآخر، بدلاً من الانتشار عبر المكون. لاحظ أيضًا أن المخطط يستخدم النوع: "تعبير" للإجمالي الفرعي والضريبة والإجمالي. التعبير للقراءة فقط ويستخدم بشكل أساسي لعرض القيم المحسوبة. يدعم SurveyJS أيضًا النوع: 'html' للمحتوى الثابت، ولكن بالنسبة للقيم المحسوبة، فإن التعبير هو الاختيار الصحيح. الآن بالنسبة للجانب رد الفعل. التقديم والتقديم بسيط جدا. قم بتوصيل onComplete إلى واجهة برمجة التطبيقات (API) الخاصة بك بنفس الطريقة — عبر useMutation أو الجلب البسيط:
استيراد { useState، useEffect، useRef } من "react"؛
وظيفة التصدير SurveyForm() { const [model] = useState(() => new Model(surveySchema));
طفرة ثابتة = useMutation({ MutationFn: غير متزامن (بيانات) => { const res = انتظار الجلب("/api/orders"، { الطريقة: "POST"، الرؤوس: { "نوع المحتوى": "application/json" }، الجسم: JSON.stringify (البيانات)، }); إذا (!res.ok) ألقى خطأ جديد("فشل الإرسال"); إرجاع res.json(); }, });
const mutationRef = useRef(mutation); mutationRef.current = طفرة؛ useEffect(() => { const Handler = (sender) => MutationRef.current.mutate(sender.data); model.onComplete.add(handler); return () => model.onComplete.remove(handler); }, [model]); // يتجنب المرجع إعادة تسجيل المعالج في كل عرض (تغيير هوية كائن الطفرة)
العودة ( <> <نموذج الاستطلاع={model} /> {mutation.isError &&
راجع Pen SurveyJS-03-SurveyJS [متشعب] بواسطة sixextinction.
يتم تشغيل onComplete عندما يصل المستخدم إلى نهاية آخر صفحة مرئية. لذا، إذا لم يتجاوز الإجمالي 100 مطلقًا وتم تخطي صفحة المراجعة، فسيتم تشغيلها بشكل صحيح لأن SurveyJS يقوم بتقييم الرؤية قبل تحديد معنى "الصفحة الأخيرة". بعد ذلك، تحتوي بيانات المرسل على جميع الإجابات بالإضافة إلى القيم المحسوبة (الإجمالي الفرعي والضريبة والإجمالي) كحقول من الدرجة الأولى، وبالتالي فإن حمولة واجهة برمجة التطبيقات مطابقة لما تم تجميعه في إصدار RHF يدويًا في onSubmit. النمط MutationRef هو نفس النمط الذي يمكنك الوصول إليه في أي مكان تحتاج فيه إلى معالج حدث مستقر على قيمة تتغير في كل عرض - لا يوجد شيء محدد في SurveyJS بشأنه.
لم يعد مكون React يحتوي على أي منطق عمل على الإطلاق. لا يوجد useWatch، ولا JSX الشرطية، ولا عداد الخطوات، ولا سلسلة useMemo، ولا superRefine. تقوم React بما تجيده بالفعل: عرض مكون وتوصيله باستدعاء واجهة برمجة التطبيقات (API). ما الذي خرج من رد الفعل؟
قلق مكدس RHF SurveyJS الرؤية فروع جي إس إكس visualIf القيم المشتقة useWatch/useMemo التعبير قواعد عبر المجال superRefine شروط المخطط الملاحة حالة الخطوة الصفحة مرئيةإذا موقع القاعدة موزعة على الملفات مركزية في المخطط
ما يبقى في React هو التخطيط، والتصميم، وأسلاك الإرسال، وتكامل التطبيق، أي الأشياء التي تم تصميم React من أجلها بالفعل. تم نقل كل شيء آخر إلى المخطط، ولأن المخطط مجرد كائن JSON، فيمكن تخزينه في قاعدة بيانات، أو إصدار إصدار مستقل عن رمز التطبيق الخاص بك، أو تحريره من خلال الأدوات الداخلية دون الحاجة إلى النشر. يمكن لمدير المنتج الذي يحتاج إلى تغيير الحد الذي يؤدي إلى تشغيل صفحة المراجعة القيام بذلك دون لمس المكون. يعد هذا اختلافًا تشغيليًا مفيدًا للفرق حيث يتطور سلوك النموذج بشكل متكرر ولا يقوده المهندسون دائمًا. متى يجب استخدام كل نهج؟ إليك قاعدة جيدة تناسبني: تخيل حذف النموذج بالكامل. ماذا ستخسر؟
إذا كان الأمر عبارة عن شاشات، فأنت تريد نماذج تعتمد على المكونات. إذا كان منطق الأعمال، مثل الحدود والقواعد المتفرعة والمتطلبات الشرطية التي تشفر القرارات الحقيقية، فأنت تريد محرك مخطط.
وبالمثل، إذا كانت التغييرات القادمة في طريقك تتعلق في الغالب بالتسميات والحقول والتخطيط، فإن RHF سوف يخدمك بشكل جيد. إذا كانت تتعلق بالشروط والنتائج والقواعد التي قد يحتاج فريق العمليات أو الفريق القانوني لديك إلى تعديلها بعد ظهر يوم الثلاثاء دون تقديم تذكرة، فإن نموذج المخطط مع SurveyJS هو الأكثر صدقًا. وهذان النهجان لا يتنافسان حقًا مع بعضهما البعض. إنها تعالج فئات مختلفة من المشاكل، والخطأ الذي يستحق تجنبه هو عدم تطابق التجريد مع وزن المنطق - التعامل مع نظام القواعد كمكون لأنه أداة مألوفة، أو الوصول إلى محرك السياسة لأن النموذج نما إلى ثلاث خطوات واكتسب حقلاً شرطيًا. النموذج الذي بنيناه هنا يقع بالقرب من الحدود بشكل متعمد، وهو معقد بما يكفي لكشف الفرق ولكن ليس متطرفًا لدرجة أن المقارنة تبدو مزيفة. من المحتمل أن تكون معظم الأشكال الحقيقية التي أصبحت غير عملية في قاعدة التعليمات البرمجية الخاصة بك موجودة بالقرب من نفس الحدود، وعادةً ما يكون السؤال هو ما إذا كان أي شخص قد قام بتسمية ما هي عليه بالفعل. استخدم React Hook Form + Zod عندما:
النماذج موجهة نحو CRUD؛ المنطق ضحل ويحركه واجهة المستخدم. يمتلك المهندسون كل السلوك؛ وتبقى الواجهة الخلفية هي مصدر الحقيقة.
استخدم SurveyJS عندما:
النماذج ترمز لقرارات العمل؛ تتطور القواعد بشكل مستقل عن واجهة المستخدم؛ يجب أن يكون المنطق مرئيًا أو قابلاً للتدقيق أو إصدارًا؛ يؤثر غير المهندسين على السلوك؛ يجب أن يتم تشغيل نفس النموذج عبر واجهات أمامية متعددة.