Artikel ini ditaja oleh SurveyJS Terdapat model mental yang dikongsi oleh kebanyakan pembangun React tanpa membincangkannya dengan lantang. Borang itu sentiasa sepatutnya menjadi komponen. Ini bermakna timbunan seperti:

Borang Cangkuk Bertindak balas untuk keadaan setempat (pemarahan semula minimum, pendaftaran medan ergonomik, interaksi penting). Zod untuk pengesahan (ketepatan input, pengesahan sempadan, penghuraian selamat jenis). React Query untuk backend: penyerahan, cuba semula, caching, penyegerakan pelayan dan sebagainya.

Dan untuk sebahagian besar borang — skrin log masuk anda, halaman tetapan anda, mod CRUD anda — ini berfungsi dengan baik. Setiap bahagian melakukan tugasnya, mereka mengarang dengan bersih, dan anda boleh beralih ke bahagian aplikasi anda yang sebenarnya membezakan produk anda. Tetapi sekali-sekala, borang mula mengumpul perkara seperti peraturan keterlihatan yang bergantung pada jawapan terdahulu atau nilai terbitan yang mengalir melalui tiga medan. Mungkin juga keseluruhan halaman yang harus dilangkau atau ditunjukkan berdasarkan jumlah yang sedang berjalan. Anda mengendalikan bersyarat pertama dengan useWatch dan cawangan sebaris, yang tidak mengapa. Kemudian yang lain. Kemudian anda mendapatkan superRefine untuk mengekod peraturan merentas medan yang skema Zod anda tidak dapat dinyatakan dengan cara biasa. Kemudian, navigasi langkah mula membocorkan logik perniagaan. Pada satu ketika, anda melihat apa yang telah anda bina dan menyedari bahawa borang itu bukan UI lagi. Ia lebih kepada proses keputusan, dan pokok komponen adalah tempat anda menyimpannya. Di sinilah saya fikir model mental untuk borang dalam React rosak, dan ini sebenarnya bukan salah sesiapa. Timbunan RHF + Zod sangat baik untuk kegunaannya. Isunya ialah kita cenderung untuk terus menggunakannya melepasi titik di mana abstraksinya sepadan dengan masalah kerana alternatif itu memerlukan cara pemikiran yang berbeza tentang bentuk sepenuhnya. Artikel ini adalah mengenai alternatif itu. Untuk menunjukkan ini, kami akan membina borang berbilang langkah yang sama dua kali:

Dengan React Hook Form + Zod berwayar ke React Query untuk penyerahan, Dengan SurveyJS, yang menganggap borang sebagai data — skema JSON yang mudah — dan bukannya pokok komponen.

Keperluan yang sama, logik bersyarat yang sama, panggilan API yang sama pada penghujungnya. Kemudian kami akan memetakan dengan tepat apa yang telah dipindahkan dan apa yang kekal, dan menyediakan cara praktikal untuk menentukan model yang anda patut gunakan, dan bila. Borang yang kami bina:

Borang ini akan menggunakan aliran 4 langkah: Langkah 1: Butiran

Nama pertama (diperlukan), E-mel (diperlukan, format yang sah).

Langkah 2: Pesanan

harga seunit, Kuantiti, Kadar cukai, Diperoleh: Jumlah kecil, cukai, Jumlah.

Langkah 3: Akaun & Maklum Balas

Adakah anda mempunyai akaun? (Ya/Tidak) Jika Ya → nama pengguna + kata laluan, kedua-duanya diperlukan. Jika Tidak → e-mel sudah dikumpulkan dalam langkah 1.

Penilaian kepuasan (1–5) Jika ≥ 4 → tanya “Apa yang anda suka?” Jika ≤ 2 → tanya “Apakah yang boleh kami perbaiki?”

Langkah 4: Semakan

Hanya muncul jika jumlah >= 100 Penyerahan akhir.

Ini tidak melampau. Tetapi ia cukup untuk mendedahkan perbezaan seni bina. Bahagian 1: Didorong Komponen (Borang Cangkuk Reaksi + Zod) Pemasangan npm pasang react-hook-form zod @hookform/resolvers @tanstack/react-query

Skema Zod Mari kita mulakan dengan skema Zod, kerana biasanya di situlah bentuk bentuk terbentuk. Untuk dua langkah pertama — butiran peribadi dan input pesanan — semuanya adalah mudah: rentetan yang diperlukan, nombor dengan minimum dan enum. Bahagian yang menarik bermula apabila anda cuba menyatakan peraturan bersyarat.

import { z } daripada "zod";

eksport const formSchema = z.objek({Nama pertama: z.string().min(1, "Diperlukan"), e-mel: z.string().e-mel("E-mel tidak sah"), harga: z.number().min(0), kuantiti: z.number().min(1), Kadar cukai: z.number(), hasAccount:z.enum" kata laluan: z.string().optional(), kepuasan: z.number().min(1).max(5), positiveFeedback: z.string().optional(), improvementFeedback: z.string().optional(),}).superRefine((data, ctx) => { if (data.hasAccount === "Ya") { if (!data.username) { s. ["nama pengguna"], mesej: "Diperlukan" }); } jika (!data.password || data.password.length < 6) { ctx.addIssue({ code: "custom", path: ["password"], message: "Min 6 characters" } }

jika (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ code: "custom", path: ["positiveFeedback"], message: "Sila kongsi perkara yang anda suka" }); }

if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ code: "custom", path:["ImprovementFeedback"], mesej: "Sila beritahu kami perkara yang perlu diperbaiki" }); }});

jenis eksport FormData = z.infer;

Perhatikan bahawa nama pengguna dan kata laluan ditaip sebagai pilihan() walaupun ia diperlukan secara bersyarat kerana skema peringkat jenis Zod menerangkan bentuk objek, bukan peraturan yang mengawal apabila medan penting. Keperluan bersyarat perlu hidup di dalam superRefine, yang dijalankan selepas bentuk disahkan dan mempunyai akses kepada objek penuh. Perpisahan itu bukanlah satu kecacatan; ia hanya untuk alat ini: superRefine ialah tempat logik merentas medan pergi apabila ia tidak boleh dinyatakan dalam struktur skema itu sendiri. Perkara yang juga ketara di sini ialah perkara yang tidak dinyatakan oleh skema ini. Ia tidak mempunyai konsep halaman, tiada konsep medan yang boleh dilihat pada titik mana, dan tiada konsep navigasi. Semua itu akan tinggal di tempat lain. Komponen Borang

import { useForm, useWatch } daripada "react-hook-form";import { zodResolver } daripada "@hookform/resolvers/zod";import { useMutation } daripada "@tanstack/react-query";import { useState, useMemo } daripada "react";import { formSchema, taip FormData }"

const STEPS = ["perincian", "pesanan", "akaun", "semakan"];

taip OrderPayload = FormData & { subtotal: number; cukai: nombor; jumlah: bilangan };

fungsi eksport RHFMultiStepForm() { const [step, setStep] = useState(0);

mutasi const = useMutation({ mutationFn: async (muatan muatan: OrderPayload) => { const res = await fetch("/api/orders", { kaedah: "POST", pengepala: { "Content-Type": "application/json" }, badan: JSON.stringify(payload), }); jika (!res.ok) buang Ralat baharu("Gagal menyerahkan"); return res.json(); }, });

const { register, control, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(formSchema), defaultValues: { price: 0, quantity: 1, taxRate: 0.1, satisfaction: 3, hasAccount: "No", }, harga const = useWatch({kawalan, nama: "harga" }); const quantity = useWatch({ control, name: "kuantiti" }); const taxRate = useWatch({ control, name: "taxRate" }); const hasAccount = useWatch({ control, name: "hasAccount" }); const kepuasan = useWatch({ kawalan, nama: "kepuasan" }); const subtotal = useMemo(() => (harga ?? 0) * (kuantiti ?? 1), [harga, kuantiti]); cukai const = useMemo(() => subtotal * (taxRate ?? 0), [subtotal, taxRate]); const total = useMemo(() => subtotal + cukai, [subtotal, tax]); const onSubmit = (data: FormData) => mutation.mutate({ ...data, subtotal, tax, total }); const showSubmit = (langkah === 2 && jumlah < 100) || (langkah === 3 && jumlah >= 100)

pulangkan (

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

{langkah === 1 && ( <>

Jumlah kecil: {subtotal}
Cukai: {cukai}
Jumlah: {total}
)}

{step === 2 && ( <>

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

{kepuasan >= 4 && (