هذه المقالة برعاية 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({ المحلّل: zodResolver(formSchema), defaultValues: { السعر: 0, الكمية: 1, معدل الضريبة: 0.1, الرضا: 3, hasAccount: "No", }, }); const Price = useWatch({ control, name: "price" }); كمية ثابتة = useWatch({ التحكم، الاسم: "الكمية" }); const TaxRate = useWatch({ control, name: "taxRate" }); const hasAccount = useWatch({ control, name: "hasAccount" }); رضا ثابت = useWatch({ control, name: "satisfaction" }); const subtotal = useMemo(() => (price ?? 0) * (quantity ?? 1), [price,Quality]); const Tax = useMemo(() => المجموع الفرعي * (taxRate ?? 0), [subtotal, TaxRate]); const Total = useMemo(() => المجموع الفرعي + الضريبة, [المجموع الفرعي, الضريبة]); const onSubmit = (data: FormData) => Mutate.mutate({ ...data, subtotal, Tax, Total }); const showSubmit = (الخطوة === 2 && الإجمالي < 100) || (الخطوة === 3 && الإجمالي >= 100)

return (

{step === 0 && ( <> )}

{step === 1 && ( <>

الإجمالي الفرعي: {subtotal
الضريبة: {tax}
الإجمالي: {total}
)}

{الخطوة === 2 && ( <>

{hasAccount === "Yes" && ( <> )}

{الرضا >= 4 && (