บทความนี้ได้รับการสนับสนุนจาก SurveyJS มีโมเดลทางจิตที่นักพัฒนา React ส่วนใหญ่แบ่งปันโดยไม่เคยพูดคุยเรื่องนี้ออกมาดังๆ แบบฟอร์มนั้นควรจะเป็นส่วนประกอบเสมอ นี่หมายถึงสแต็กเช่น:

React Hook Form สำหรับรัฐในท้องถิ่น (การเรนเดอร์ซ้ำน้อยที่สุด การลงทะเบียนฟิลด์ตามหลักสรีระศาสตร์ การโต้ตอบที่จำเป็น) Zod สำหรับการตรวจสอบ (ความถูกต้องของอินพุต, การตรวจสอบขอบเขต, การแยกวิเคราะห์ประเภทที่ปลอดภัย) โต้ตอบแบบสอบถามสำหรับแบ็กเอนด์: การส่ง การลองใหม่ การแคช การซิงค์เซิร์ฟเวอร์ และอื่นๆ

และสำหรับแบบฟอร์มส่วนใหญ่ — หน้าจอเข้าสู่ระบบ หน้าการตั้งค่า โมดอล CRUD ของคุณ — สิ่งนี้ทำงานได้ดีจริงๆ แต่ละชิ้นทำงานของตัวเอง องค์ประกอบเรียบเรียงเรียบร้อย และคุณสามารถไปยังส่วนต่างๆ ของการใช้งานที่สร้างความแตกต่างให้กับผลิตภัณฑ์ของคุณได้ แต่ในบางครั้ง แบบฟอร์มจะเริ่มสะสมสิ่งต่างๆ เช่น กฎการมองเห็นที่ขึ้นอยู่กับคำตอบก่อนหน้า หรือค่าที่ได้รับซึ่งเรียงซ้อนผ่านสามฟิลด์ อาจเป็นทั้งหน้าที่ควรข้ามหรือแสดงโดยพิจารณาจากผลรวมทั้งหมด คุณจัดการเงื่อนไขแรกด้วย useWatch และสาขาแบบอินไลน์ ซึ่งถือว่าใช้ได้ แล้วอีกอย่าง. ถ้าอย่างนั้น คุณกำลังเข้าถึง superRefine เพื่อเข้ารหัสกฎข้ามฟิลด์ที่ Zod schema ของคุณไม่สามารถแสดงได้ตามปกติ จากนั้น การนำทางตามขั้นตอนจะเริ่มทำให้ตรรกะทางธุรกิจรั่วไหล เมื่อถึงจุดหนึ่ง คุณมองไปที่สิ่งที่คุณสร้างขึ้นและพบว่าแบบฟอร์มนั้นไม่ใช่ UI จริงๆ อีกต่อไป มันเป็นกระบวนการตัดสินใจมากกว่า และแผนผังส่วนประกอบเป็นเพียงที่ที่คุณจัดเก็บไว้เท่านั้น นี่คือจุดที่ฉันคิดว่าแบบจำลองทางจิตสำหรับแบบฟอร์มใน React พังทลายลง และไม่มีใครผิดเลยจริงๆ RHF + Zod Stack นั้นยอดเยี่ยมในสิ่งที่ออกแบบมาเพื่อมัน ปัญหาก็คือเรามักจะใช้มันต่อไปในจุดที่นามธรรมตรงกับปัญหา เพราะทางเลือกอื่นต้องใช้วิธีคิดที่แตกต่างออกไปเกี่ยวกับรูปแบบโดยสิ้นเชิง บทความนี้เกี่ยวกับทางเลือกนั้น เพื่อแสดงให้เห็นสิ่งนี้ เราจะสร้างแบบฟอร์มหลายขั้นตอนที่เหมือนกันทุกประการสองครั้ง:

ด้วย React Hook Form + Zod ที่เชื่อมต่อเข้ากับ React Query เพื่อการส่ง ด้วย SurveyJS ซึ่งถือว่าแบบฟอร์มเป็นข้อมูล — สคีมา JSON แบบง่าย — แทนที่จะเป็นแผนผังส่วนประกอบ

ข้อกำหนดเดียวกัน ตรรกะเงื่อนไขเดียวกัน การเรียก API เดียวกันในตอนท้าย จากนั้นเราจะจัดทำแผนที่อย่างชัดเจนว่าสิ่งใดเคลื่อนไหวและสิ่งใดยังคงอยู่ และจัดทำแนวทางที่เป็นประโยชน์ในการตัดสินใจว่าควรใช้โมเดลใดและเมื่อใด แบบฟอร์มที่เรากำลังสร้าง:

แบบฟอร์มนี้จะใช้โฟลว์ 4 ขั้นตอน: ขั้นตอนที่ 1: รายละเอียด

ชื่อ (จำเป็น) อีเมล (จำเป็น รูปแบบที่ถูกต้อง)

ขั้นตอนที่ 2: สั่งซื้อ

ราคาต่อหน่วย ปริมาณ อัตราภาษี ได้มา: ผลรวมย่อย ภาษี รวม.

ขั้นตอนที่ 3: บัญชีและคำติชม

คุณมีบัญชีแล้วหรือยัง? (ใช่/ไม่ใช่) ถ้าใช่ → ชื่อผู้ใช้ + รหัสผ่าน จำเป็นทั้งคู่ หากไม่มี → อีเมลได้รวบรวมไว้ในขั้นตอนที่ 1 แล้ว

คะแนนความพึงพอใจ (1–5) ถ้า ≥ 4 → ถามว่า “คุณชอบอะไร” ถ้า ≤ 2 → ถามว่า “เราจะปรับปรุงอะไรได้บ้าง”

ขั้นตอนที่ 4: ทบทวน

ปรากฏเฉพาะเมื่อผลรวม >= 100 การส่งครั้งสุดท้าย

นี่ไม่ใช่เรื่องสุดโต่ง แต่ก็เพียงพอที่จะเปิดเผยความแตกต่างทางสถาปัตยกรรม ส่วนที่ 1: การขับเคลื่อนด้วยคอมโพเนนต์ (React Hook Form + Zod) การติดตั้ง npm ติดตั้ง react-hook-form zod @hookform/resolvers @tanstack/react-query

ซอด สคีมา เริ่มจาก Zod schema กันก่อน เพราะโดยปกติแล้วจะเป็นจุดที่รูปร่างของแบบฟอร์มถูกสร้างขึ้น สำหรับสองขั้นตอนแรก — รายละเอียดส่วนบุคคลและการป้อนคำสั่งซื้อ — ทุกอย่างตรงไปตรงมา: สตริงที่ต้องระบุ ตัวเลขที่มีค่าต่ำสุด และแจงนับ ส่วนที่น่าสนใจเริ่มต้นเมื่อคุณพยายามแสดงกฎที่มีเงื่อนไข

นำเข้า { z } จาก "zod";

ส่งออก 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"]), ชื่อผู้ใช้: z.string().เป็นทางเลือก() รหัสผ่าน: z.string().ตัวเลือก() ความพึงพอใจ: z.number().min(1).max(5) ค่าบวกข้อเสนอแนะ: z.string().ตัวเลือก() การปรับปรุงข้อเสนอแนะ: z.string().ตัวเลือก(),}).superRefine((ข้อมูล, ctx) => { if (data.hasAccount === "ใช่") { if (!data.username) { ctx.addIssue({ รหัส: "กำหนดเอง", เส้นทาง: ["ชื่อผู้ใช้"], ข้อความ: "จำเป็น" }); } if (!data.password || data.password.length < 6) { ctx.addIssue({ code: "custom", path: ["password"], ข้อความ: "Min 6 character" });

if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ code: "custom", path: ["positiveFeedback"], ข้อความ: "โปรดแบ่งปันสิ่งที่คุณชอบ" }); }

if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ รหัส: "กำหนดเอง", เส้นทาง:["improvementFeedback"], ข้อความ: "โปรดบอกเราว่าควรปรับปรุงอะไรบ้าง" }); }});

ประเภทการส่งออก 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 [ขั้นตอน, setStep] = useState (0);

การกลายพันธุ์ const = useMutation({ mututFn: async (เพย์โหลด: OrderPayload) => { const res = รอการดึงข้อมูล ("/api/orders", { วิธีการ: "โพสต์" ส่วนหัว: { "ประเภทเนื้อหา": "application/json" }, เนื้อความ: JSON.stringify (เพย์โหลด) }); ถ้า (!res.ok) โยนข้อผิดพลาดใหม่ ("ไม่สามารถส่ง"); กลับ res.json(); }, });

const { register, control, handleSubmit, formState: { error }, } = useForm({ Resolver: zodResolver(formSchema), defaultValues: { ราคา: 0, ปริมาณ: 1, TaxRate: 0.1, ความพึงพอใจ: 3, hasAccount: "ไม่", }, }); ราคา const = useWatch ({ ควบคุมชื่อ: "ราคา" }); ปริมาณ const = useWatch ({ ควบคุมชื่อ: "ปริมาณ" }); const TaxRate = useWatch ({ ควบคุม ชื่อ: "taxRate" }); const hasAccount = useWatch ({ ควบคุม ชื่อ: "hasAccount" }); ความพึงพอใจ const = useWatch ({ ควบคุม ชื่อ: "ความพึงพอใจ" }); const subtotal = useMemo(() => (ราคา ?? 0) * (ปริมาณ ?? 1), [ราคา, ปริมาณ]); const Tax = useMemo(() => ผลรวมย่อย * (taxRate ?? 0), [ผลรวมย่อย, TaxRate]); const Total = useMemo(() => ผลรวมย่อย + ภาษี, [ผลรวมย่อย, ภาษี]); const onSubmit = (ข้อมูล: FormData) => Mutate.mutate({ ...data, ผลรวมย่อย, ภาษี, รวม }); const showSubmit = (ขั้นตอน === 2 && รวม < 100) || (ขั้นตอน === 3 && รวม >= 100)

return (

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

{step === 1 && ( <>

ผลรวมย่อย: {ผลรวมย่อย
ภาษี: {tax}
ผลรวม: {total}
)}

{ขั้นตอน === 2 && ( <>

{hasAccount === "ใช่" && ( <> )}

{ความพึงพอใจ >= 4 && (