Bu makale SurveyJS tarafından desteklenmektedir Çoğu React geliştiricisinin yüksek sesle tartışmadan paylaştığı zihinsel bir model var. Bu formların her zaman bileşenler olması gerekiyordu. Bu şuna benzer bir yığın anlamına gelir:

Yerel durum için React Hook Formu (minimum yeniden işleme, ergonomik alan kaydı, zorunlu etkileşim). Doğrulama için Zod (giriş doğruluğu, sınır doğrulama, tür açısından güvenli ayrıştırma). Arka uç için React Query: gönderme, yeniden denemeler, önbelleğe alma, sunucu senkronizasyonu vb.

Ve formların büyük çoğunluğu için (giriş ekranlarınız, ayar sayfalarınız, CRUD modelleriniz) bu gerçekten işe yarıyor. Her parça işini yapar, temiz bir şekilde oluşur ve uygulamanızın ürününüzü gerçekten farklı kılan kısımlarına geçebilirsiniz. Ancak arada bir, bir form daha önceki yanıtlara bağlı olan görünürlük kuralları veya üç alandan geçen türetilmiş değerler gibi şeyleri biriktirmeye başlar. Belki de toplam sayıya göre atlanması veya gösterilmesi gereken sayfaların tamamı bile olabilir. İlk koşulluyu bir useWatch ve satır içi dalla halledersiniz, bu da sorun değil. Sonra bir tane daha. Daha sonra Zod şemanızın normal şekilde ifade edemediği alanlar arası kuralları kodlamak için superRefine'a ulaşıyorsunuz. Ardından adım navigasyonu iş mantığını sızdırmaya başlar. Bir noktada, oluşturduğunuz şeye bakıyorsunuz ve formun artık gerçekten kullanıcı arayüzü olmadığını fark ediyorsunuz. Bu daha çok bir karar sürecidir ve bileşen ağacı tam da onu sakladığınız yerdir. React'taki formlara ilişkin zihinsel modelin burada çöktüğünü düşünüyorum ve bu aslında kimsenin hatası değil. RHF + Zod yığını, tasarlandığı amaç açısından mükemmeldir. Sorun şu ki, soyutlamaları sorunla eşleşinceye kadar onu kullanmaya devam etme eğilimindeyiz çünkü alternatif, formlar hakkında tamamen farklı bir düşünme tarzı gerektiriyor. Bu makale bu alternatif hakkındadır. Bunu göstermek için aynı çok adımlı formu iki kez oluşturacağız:

React Hook Form + Zod'un gönderim için React Query'ye bağlanmasıyla, Bir formu bileşen ağacı yerine veri (basit bir JSON şeması) olarak ele alan SurveyJS ile.

Aynı gereksinimler, aynı koşullu mantık, sonunda aynı API çağrısı. Daha sonra tam olarak neyin taşındığını ve neyin kaldığını haritalandıracağız ve hangi modeli ne zaman kullanmanız gerektiğine karar vermenin pratik bir yolunu ortaya koyacağız. Oluşturduğumuz form:

Bu formda 4 adımlı bir akış kullanılacaktır: 1. Adım: Ayrıntılar

Ad (gerekli), E-posta (gerekli, geçerli format).

Adım 2: Sipariş Verin

Birim fiyatı, miktar, Vergi oranı, Türetilmiş: Ara toplam, vergi, Toplam.

3. Adım: Hesap ve Geri Bildirim

Hesabınız var mı? (Evet/Hayır) Evet ise → kullanıcı adı + şifre, her ikisi de gereklidir. Hayır ise → e-posta zaten 1. adımda toplandı.

Memnuniyet derecesi (1-5) ≥ 4 ise → “Neyi beğendin?” diye sorun. ≤ 2 ise → “Neyi geliştirebiliriz?” diye sorun.

4. Adım: İnceleme

Yalnızca toplam >= 100 ise görünür Son teslim.

Bu aşırı değil. Ancak mimari farklılıkları ortaya çıkarmak yeterlidir. Bölüm 1: Bileşen Odaklı (React Hook Formu + Zod) Kurulum npm react-hook-form zod'u kurun @hookform/resolvers @tanstack/react-query

Zod Şeması Zod şemasıyla başlayalım çünkü genellikle formun şekli burada oluşturulur. İlk iki adımda (kişisel ayrıntılar ve sipariş girişleri) her şey basittir: gerekli dizeler, minimumlu sayılar ve bir numaralandırma. İşin ilginç kısmı koşullu kuralları ifade etmeye çalıştığınızda başlıyor.

{ z }'yi "zod"dan içe aktarın;

const formSchema = z.object({ FirstName: z.string().min(1, "Gerekli"), email: z.string().email("Geçersiz e-posta"), fiyat: z.number().min(0), miktar: z.number().min(1), TaxRate: z.number(), hasAccount: z.enum(["Evet", "Hayır"]), kullanıcı adı: z.string().opsiyonel(), şifre: z.string().isteğe bağlı(), memnuniyet: z.number().min(1).max(5), pozitifFeedback: z.string().isteğe bağlı(), iyileştirmeFeedback: z.string().isteğe bağlı(),}).superRefine((data, ctx) => { if (data.hasAccount === "Evet") { if (!data.username) { ctx.addIssue({ code: "custom", path: ["kullanıcı adı"], mesaj: "Gerekli" }); if (!data.password || data.password.length < 6) { ctx.addIssue({ code: "özel", yol: ["şifre"], mesaj: "Min 6 karakter" } } }

if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ code: "custom", path: ["positiveFeedback"], message: "Lütfen beğendiklerinizi paylaşın" }); }

if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ code: "özel", yol:["improvementFeedback"], mesaj: "Lütfen bize neyi geliştirmemiz gerektiğini söyleyin" }); }});

dışa aktarma türü FormData = z.infer;

Zod'un tür düzeyindeki şeması, alanların ne zaman önemli olduğunu belirleyen kuralları değil, nesnenin şeklini tanımladığından, koşullu olarak gerekli olmalarına rağmen kullanıcı adı ve parolanın isteğe bağlı() olarak yazıldığına dikkat edin. Koşullu gereksinimin, şekil doğrulandıktan sonra çalışan ve nesnenin tamamına erişebilen superRefine içinde bulunması gerekir. Bu ayrılık bir kusur değil; araç tam da bunun için tasarlandı: superRefine, şema yapısının kendisinde ifade edilemediğinde alanlar arası mantığın gittiği yerdir. Burada dikkat çeken şey, bu şemanın neyi ifade etmediğidir. Sayfa kavramı, hangi alanların hangi noktada görülebileceği kavramı ve gezinme kavramı yoktur. Bunların hepsi başka bir yerde yaşayacak. Form Bileşeni

"react-hook-form"dan { useForm, useWatch }'i içe aktar; "@hookform/resolvers/zod"'dan { zodResolver }'ı içe aktar; "@tanstack/react-query"'den { useMutation }'ı içe aktar; "react"'den { useState, useMemo }'yi içe aktar; "./schema"'dan { formSchema, type FormData }'yı içe aktar;

const ADIMLAR = ["ayrıntılar", "sipariş", "hesap", "inceleme"];

OrderPayload yazın = FormData & { subtotal: number; vergi: sayı; toplam: sayı };

dışa aktarma işlevi RHFMultiStepForm() { const [step, setStep] = useState(0);

const mutasyon = useMutation({ mutasyonFn: eşzamansız (yük: OrderPayload) => { const res = wait fetch("/api/orders", { yöntem: "POST", başlıklar: { "Content-Type": "application/json" }, gövde: JSON.stringify(yük), }); if (!res.ok) throw new Error("Gönderme başarısız oldu"); res.json()'u döndürün; }, });

const { kayıt, kontrol, tanıtıcıSubmit, formState: { hatalar }, } = useForm({ çözümleyici: zodResolver(formSchema), defaultValues: { fiyat: 0, miktar: 1, vergiRate: 0,1, memnuniyet: 3, hasAccount: "Hayır", }, }); const fiyat = useWatch({ kontrol, isim: "fiyat" }); const miktar = useWatch({ kontrol, isim: "miktar" }); const vergiRate = useWatch({ control, name: "taxRate" }); const hasAccount = useWatch({ control, name: "hasAccount" }); const memnuniyeti = useWatch({ kontrol, isim: "memnuniyet" }); const alt toplam = useMemo(() => (fiyat ?? 0) * (miktar ?? 1), [fiyat, miktar]); const vergi = useMemo(() => alt toplam * (vergiOranı ?? 0), [alttoplam, vergiOranı]); const toplam = useMemo(() => alt toplam + vergi, [alt toplam, vergi]); const onSubmit = (data: FormData) => mutasyon.mutate({ ...data, subtotal, vergi, total }); const showSubmit = (adım === 2 && toplam < 100) || (adım === 3 && toplam >= 100)

return (

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

{step === 1 && ( <>

Alttoplam: {subtotal
Vergi: {tax
Toplam: {toplam
)}

{step === 2 && ( <>

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

{memnuniyet >= 4 && (