Bu makale sponsorluğundadırSurveyJS
Çoğu React geliştiricisinin yüksek sesle tartışmadan paylaştığı zihinsel bir model var. Bu formlarHer zamanbileşenler olması gerekiyor. Bu şuna benzer bir yığın anlamına gelir:
- Tepki Kanca Formuyerel durum için (minimum yeniden oluşturma, ergonomik alan kaydı, zorunlu etkileşim).
- Zoddoğrulama için (giriş doğruluğu, sınır doğrulama, tür açısından güvenli ayrıştırma).
- Tepki Sorgusuarka uç için: 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 ile halledersinizizle izleve bir satır içi dal, ki bu iyi. Sonra bir tane daha.O zaman ulaşıyorsunsüper rafineZod şemanızın normal şekilde ifade edemediği alanlar arası kuralları kodlamak için. 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ını 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 şu durumlarda görünür:
toplam >= 100 - 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({'i dışa aktar
ad: z.string().min(1, "Gerekli"),
e-posta: z.string().email("Geçersiz e-posta"),
fiyat: z.number().min(0),
miktar: z.number().min(1),
vergiRate: z.number(),
hasAccount: z.enum(["Evet", "Hayır"]),
Kullanıcı adı: z.string().isteğe bağlı(),
şifre: z.string().isteğe bağlı(),
memnuniyet: z.number().min(1).max(5),
PositiveFeedback: z.string().isteğe bağlı(),
iyileştirmeGeri Bildirim: z.string().isteğe bağlı(),
}).superRefine((veri, ctx) => {
if (data.hasAccount === "Evet") {
if (!data.kullanıcıadı) {
ctx.addIssue({ code: "özel", yol: ["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: "custom", path: ["improvementFeedback"], message: "Lütfen bize neyi geliştirmemiz gerektiğini söyleyin" });
}
});
dışa aktarma türü FormData = z.infer;
Şuna dikkat edinkullanıcı adıVeşifreolarak yazılıristeğe bağlı()şartlı olarak gerekli olmalarına rağmen Zod'un tür düzeyindeki şeması şunları açıklamaktadır:şekilalanların ne zaman önemli olduğunu belirleyen kurallar değil, nesnenin durumu.
Şartlı şart içeride yaşamak zorundasüper rafineŞekil doğrulandıktan sonra çalışır ve nesnenin tamamına erişime sahiptir. Bu ayrılık bir kusur değil; araç tam da bunun için tasarlandı:süper rafineşema yapısının kendisinde ifade edilemediğinde alanlar arası mantığın gittiği yerdir.
Burada ayrıca dikkat çekici olan şey, bu şemanın ne olduğudur.değilifade etmek. 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çe aktarın;
{ zodResolver }'ı "@hookform/resolvers/zod" adresinden içe aktarın;
{ useMutation }'ı "@tanstack/react-query"den içe aktarın;
{ useState, useMemo }'yu "react"tan içe aktarın;
{ formSchema'yı içe aktarın, "./schema"dan FormData yazın;
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 [adım, 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;
},
});
sabit {
kayıt olmak,
kontrol,
tanıtıcıGönder,
formState: { hatalar },
} = useForm({
çözümleyici: zodResolver(formSchema),
varsayılanDeğerler: {
fiyat: 0,
miktar: 1,
vergiOranı: 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)
dönüş (
);
}
Kalemi görünSurveyJS-03-RHF [çatallı]ilealtıncı yok oluş.
Burada pek çok şey oluyor ve olayların nereye vardığını görmek için yavaşlamaya değer.
- Türetilmiş değerler —
ara toplam,vergi,toplam— bileşende şu şekilde hesaplanır:izle izleVeKullanım NotuÇünkü bunlar canlı saha değerlerine bağlıdır ve bunların başka doğal bir yeri yoktur. - Görünürlük kuralları
kullanıcı adı,şifre,Olumlu Geribildirim, VeiyileştirmeGeri bildirimJSX'te satır içi koşul cümleleri olarak yaşarsınız. - Adım atlama mantığı — inceleme sayfası yalnızca
toplam >= 100— içine gömülüdürgösterGönderdeğişkeni ve 3. adımdaki oluşturma koşulunu seçin. - Navigasyonun kendisi sadece bir
Kullanım Durumumanuel olarak artırdığımız sayaç. - React Query yeniden denemeleri, önbelleğe almayı ve geçersiz kılmayı yönetir. Form sadece çağırıyor
mutasyon.mutasyondoğrulanmış verilerle.
Bunların hiçbiri değilyanlış,kendi başına. Bu hala deyimsel React'tır ve RHF'nin yeniden oluşturmayı nasıl izole ettiği sayesinde bileşen oldukça performanslıdır.
Ama bunu daha önce yazmamış birine verip açıklamasını isteseydinizinceleme sayfası hangi koşullar altında görünür?izini sürmek zorunda kalacaklardıgösterGönder, 3. adımın oluşturma koşulu ve gezinme düğmesi mantığı (üç ayrı yer) tek satırda ifade edilebilecek bir kuralı yeniden oluşturmak için kullanılır.
Form çalışıyor evet, ancak davranış bir sistem olarak gerçekten denetlenemez.Zihinsel olarak yürütülmesi gerekiyor.
Daha da önemlisi, onu değiştirmek mühendislik katılımını gerektirir. İnceleme adımı göründüğünde ayarlama yapmak gibi küçük bir değişiklik bile bileşeni düzenlemek, doğrulamayı güncellemek, bir çekme isteği açmak, incelemeyi beklemek ve yeniden dağıtmak anlamına gelir.
Bölüm 2: Şema Odaklı (SurveyJS)Şimdi aynı akışı bir şema kullanarak oluşturalım.
Kurulum
npm anket-çekirdek anket-tepki-ui'sini yükleyin @tanstack/react-query
anket çekirdeği
SurveyJS'in form oluşturma işlemini destekleyen MIT lisanslı, platformdan bağımsız çalışma zamanı motoru; burada önemsediğimiz kısım. Bir JSON şeması alır, ondan dahili bir model oluşturur ve normalde React bileşeninizde yer alacak her şeyi yönetir: görünürlük ifadelerini değerlendirmek, türetilmiş değerleri hesaplamak, sayfa durumunu yönetmek, doğrulamayı izlemek ve gerçekte hangi sayfaların gösterildiğine göre "tamamlanmanın" ne anlama geldiğine karar vermek.anket-tepki-ui
Bu modeli React'e bağlayan kullanıcı arayüzü/oluşturma katmanı. Bu aslında birMotorun durumu değiştiğinde yeniden oluşturulan bileşen. SurveyJS kullanıcı arayüzü kütüphaneleri ayrıca aşağıdakiler için de mevcuttur:Açısal,Vue3ve diğer birçok çerçeve.
Birlikte, tek bir kontrol akışı satırı yazmanıza gerek kalmadan, tamamen işlevsel, çok sayfalı bir form çalışma zamanı sağlarlar.
Şema formatının kendisi, daha önce de söylediğimiz gibi, yalnızca bir JSON'dur; DSL veya tescilli herhangi bir şey yoktur. Bunu satır içi yapabilir, bir dosyadan içe aktarabilir, bir API'den alabilir veya bir veritabanı sütununda saklayabilir ve çalışma zamanında nemlendirebilirsiniz.
Veri Olarak Aynı Form
İşte aynı form, bu sefer bir JSON nesnesi olarak ifade edildi. Şema her şeyi tanımlar: yapı, doğrulama, görünürlük kuralları, türetilmiş hesaplamalar, sayfada gezinme ve bunu bir uzmana teslim eder.Modelibunu çalışma zamanında değerlendirir. İşte tam olarak neye benziyor:
const anketSchema'yı dışa aktar = {
başlık: "Sipariş Akışı",
showProgressBar: "üst",
sayfalar: [
{
ad: "ayrıntılar",
öğeler: [
{ type: "text", name: "firstName", isRequired: true },
{ type: "text", name: "email", inputType: "email", isRequired: true, doğrulayıcılar: [{ type: "email", text: "Geçersiz e-posta" }] }
]
},
{
ad: "sipariş",
öğeler: [
{ type: "text", name: "price", inputType: "number", defaultValue: 0 },
{ type: "text", name: "quantity", inputType: "number", defaultValue: 1 },
{
şunu yazın: "açılır menü",
ad: "vergiOranı",
varsayılanDeğer: 0,1,
seçimler: [
{ değer: 0,05, metin: "%5" },
{ değer: 0,1, metin: "%10" },
{ değer: 0,15, metin: "%15" }
]
},
{
tür: "ifade",
ad: "alt toplam",
ifade: "{price}{miktar}"
},
{
tür: "ifade",
ad: "vergi",
ifade: "{alt toplam}{taxRate}"
},
{
tür: "ifade",
ad: "toplam",
ifade: "{alt toplam} + {vergi}"
}
]
},
{
ad: "hesap",
öğeler: [
{
tür: "radyo grubu",
isim: "Hesabı var",
seçenekler: ["Evet", "Hayır"]
},
{
şunu yazın: "metin",
isim: "kullanıcı adı",
görünürIf: "{hasAccount} = 'Evet'",
Gerekli: doğru
},
{
şunu yazın: "metin",
isim: "şifre",
inputType: "şifre",
görünürIf: "{hasAccount} = 'Evet'",
Gerekli: doğru,
doğrulayıcılar: [{ type: "text", minLength: 6, text: "Min 6 karakter" }]
},
{
tür: "derecelendirme",
adı: "memnuniyet",
oranMin: 1,
OranMaksimum: 5
},
{
şunu yazın: "yorum",
ad: "pozitifgeribildirim",
görünürIf: "{memnuniyet} >= 4"
},
{
şunu yazın: "yorum",
ad: "iyileştirmeGeri Bildirimi",
görünürIf: "{memnuniyet} <= 2"
}
]
},
{
ad: "inceleme",
görünürEğer: "{toplam} >= 100",
öğeler: []
}
]
};
Bir an için bunu RHF versiyonuyla karşılaştırın.
- The
süper rafineşartlı olarak gerekli olanı engellekullanıcı adıVeşifregitti.görünürIf: "{hasAccount} = 'Evet'"ile kombineGerekli: doğruher iki endişeyi de sahada, onları bulmayı umduğunuz yerde birlikte ele alır. - The
izle izle+Kullanım Notuhesaplanan zincirara toplam,vergi, Vetoplamüç ile değiştirilirifadebirbirlerine adlarıyla başvuran alanlar. - RHF sürümünde yalnızca izleme yoluyla yeniden oluşturulabilen inceleme sayfası durumu
gösterGönder, adım 3 işleme dalı. - Ve son olarak, gezinme düğmesi mantığı tek bir
görünür isesayfa nesnesindeki özellik.
Aynı mantık orada da mevcut. Sadece şema, bileşene yayılmak yerine, izole olarak görülebileceği bir yaşam alanı sağlıyor.
Ayrıca şemanın şunu kullandığını unutmayın:tür: 'ifade'alt toplam, vergi ve toplam için.İfadesalt okunurdur ve esas olarak hesaplanan değerleri görüntülemek için kullanılır. SurveyJS ayrıca şunları da destekler:şunu yazın: 'html'statik içerik için, ancak hesaplanan değerler için,ifadedoğru seçimdir.
Şimdi React tarafına geçelim.
Oluşturma ve Gönderim
Çok basit. TeltamamlandığındaAPI'nize aynı şekilde - aracılığıylaMutasyonu kullanveya sadegidip getirmek:
"react" öğesinden { useState, useEffect, useRef }'i içe aktarın;
{ useMutation }'ı "@tanstack/react-query"den içe aktarın;
"anket çekirdeğinden" { Model }'i içe aktarın;
{ Survey }'i "survey-react-ui"den içe aktarın;
"anket-core/survey-core.css" dosyasını içe aktarın;
dışa aktarma işlevi SurveyForm() {
const [model] = useState(() => new Model(surveySchema));
const mutasyon = useMutation({
mutasyonFn: eşzamansız (veri) => {
const res = wait fetch("/api/orders", {
yöntem: "POST",
başlıklar: { "Content-Type": "application/json" },
gövde: JSON.stringify(veri),
});
if (!res.ok) throw new Error("Gönderme başarısız oldu");
res.json()'u döndürün;
},
});
const mutasyonRef = useRef(mutasyon);
mutasyonRef.current = mutasyon;
useEffect(() => {
const işleyici = (gönderen) => mutasyonRef.current.mutate(gönderen.data);
model.onComplete.add(işleyici);
return () => model.onComplete.remove(işleyici);
}, [model]); // ref, her işlemede işleyicinin yeniden kaydedilmesini önler (mutasyon nesnesi kimliği değişir)
dönüş (
<>
{mutation.isError && Hata: {mutation.error.message}
>
);
}
Kalemi görünSurveyJS-03-SurveyJS [çatallı]ilealtıncı yok oluş.
tamamlandığındakullanıcı sonuncunun sonuna ulaştığında tetiklenirgörünürsayfa. Yani eğertoplamHiçbir zaman 100'ü geçmese ve inceleme sayfası atlansa bile SurveyJS "son sayfanın" ne anlama geldiğine karar vermeden önce görünürlüğü değerlendirdiği için yine de doğru şekilde tetiklenir.- Daha sonra,
gönderen.verihesaplanan değerlerle birlikte tüm yanıtları içerir (ara toplam,vergi,toplam) birinci sınıf alanlar olarak kullanılır; dolayısıyla API yükü, RHF sürümünün manuel olarak bir araya getirdiğiyle aynıdır.Gönderildiğinde. - The
mutasyonRefkalıp, her işlemede değişen bir değer üzerinden istikrarlı bir olay işleyicisine ihtiyaç duyduğunuz her yerde ulaşacağınız kalıpla aynıdır; SurveyJS'e özgü bir şey değildir.
React bileşeni artık hiçbir iş mantığı içermiyor. hayırizle izle, koşullu JSX yok, adım sayacı yok, hayırKullanım Notuzincir, hayırsüper rafine. React, aslında iyi olduğu şeyi yapıyor: bir bileşeni oluşturmak ve onu bir API çağrısına bağlamak.
| Kaygı | RHF Yığını | SurveyJS |
|---|---|---|
| Görünürlük | JSX şubeleri | görünür ise |
| Türetilmiş değerler | izle izle/Kullanım Notu |
ifade |
| Alanlar arası kurallar | süper rafine |
Şema koşulları |
| Navigasyon | adımdurum |
Sayfagörünür ise |
| Kural konumu | Dosyalar arasında dağıtılmış | Şemada merkezileştirilmiş |
React'ta kalanlar düzen, stil, gönderim kablolaması ve uygulama entegrasyonudur;React'ın gerçekte tasarlandığı şeyler.
Geriye kalan her şey şemaya taşındı ve şema yalnızca bir JSON nesnesi olduğundan, bir veritabanında saklanabilir, uygulama kodunuzdan bağımsız olarak sürümlendirilebilir veya dağıtım gerektirmeden dahili araçlar aracılığıyla düzenlenebilir.
İnceleme sayfasını tetikleyen eşiği değiştirmesi gereken bir ürün yöneticisi, bunu bileşene dokunmadan yapabilir. Bu, form davranışının sıklıkla geliştiği ve her zaman mühendisler tarafından yönlendirilmediği ekipler için anlamlı bir operasyonel farktır.
Her Yaklaşım Ne Zaman Kullanılmalı?İşte benim için işe yarayan iyi bir temel kural:formu tamamen silmeyi hayal edin. Ne kaybedersin?
- Eğer ekranlar ise, bileşen odaklı formlar istersiniz.
- Gerçek kararları kodlayan eşikler, dallanma kuralları ve koşullu gereksinimler gibi iş mantığıysa bir şema motoru istersiniz.
Benzer şekilde, önünüze çıkan değişiklikler çoğunlukla etiketler, alanlar ve düzen ile ilgiliyse, RHF size iyi hizmet edecektir. Operasyonlarınızın veya hukuk ekibinizin Salı öğleden sonra herhangi bir bildirimde bulunmadan ayarlaması gerekebilecek koşullar, sonuçlar ve kurallarla ilgiliyse SurveyJS'nin şema modeli daha dürüst bir seçimdir.
Bu iki yaklaşım aslında birbiriyle rekabet halinde değildir.Farklı sorun sınıflarına hitap ediyorlar ve kaçınılması gereken hata, soyutlamayı mantığın ağırlığıyla eşleştirmemektir - tanıdık bir araç olduğu için bir kural sistemine bir bileşen gibi davranmak veya bir form üç adıma ulaştığı ve koşullu bir alan kazandığı için bir politika motoruna ulaşmak.
Burada oluşturduğumuz form kasıtlı olarak sınıra yakın duruyor; farkı açığa çıkaracak kadar karmaşık ama karşılaştırmanın hileli olduğu hissini verecek kadar aşırı değil. Kod tabanınızda kullanışsız hale gelen gerçek formların çoğu muhtemelen aynı sınırın yakınında yer alır ve soru genellikle birinin gerçekte ne olduklarını adlandırıp adlandırmadığıdır.
React Hook Form + Zod'u şu durumlarda kullanın:
- Formlar CRUD odaklıdır;
- Mantık yüzeyseldir ve kullanıcı arayüzüne dayalıdır;
- Mühendisler tüm davranışların sahibidir;
- Arka uç gerçeğin kaynağı olmaya devam ediyor.
SurveyJS'yi şu durumlarda kullanın:
- Formlar iş kararlarını kodlar;
- Kurallar kullanıcı arayüzünden bağımsız olarak gelişir;
- Mantık görünür, denetlenebilir veya versiyonlanmış olmalıdır;
- Mühendis olmayanlar davranışı etkiler;
- Aynı formun birden fazla ön uçta çalışması gerekir.




