Ovaj članak sponzorira SurveyJS Postoji mentalni model koji većina React programera dijeli, a da o tome nikada ne razgovaraju naglas. Te forme bi uvijek trebale biti komponente. To znači stog poput:
React Hook Form za lokalno stanje (minimalni ponovni prikazi, registracija ergonomskog polja, imperativna interakcija). Zod za provjeru valjanosti (ispravnost unosa, provjera granice, raščlanjivanje bezbjednog tipa). React Query za backend: podnošenje, ponovni pokušaji, keširanje, sinhronizacija servera, itd.
A za ogromnu većinu obrazaca – vaše ekrane za prijavu, vaše stranice s postavkama, vaše CRUD modale – ovo funkcionira jako dobro. Svaki komad radi svoj posao, komponuje se čisto, a možete prijeći na dijelove vaše aplikacije koji zapravo razlikuju vaš proizvod. Ali s vremena na vrijeme, obrazac počinje akumulirati stvari kao što su pravila vidljivosti koja zavise od ranijih odgovora ili izvedene vrijednosti koje kaskadiraju kroz tri polja. Možda čak i čitave stranice koje bi trebalo preskočiti ili prikazati na osnovu tekućeg zbroja. S prvim uslovom rukujete pomoću useWatch-a i inline grane, što je u redu. Onda još jedan. Zatim posežete za superRefine da kodira pravila unakrsnih polja koja vaša Zod shema ne može izraziti na normalan način. Zatim, navigacija koraka počinje da curi poslovnu logiku. U nekom trenutku, pogledate šta ste napravili i shvatite da forma više nije UI. To je više proces odlučivanja, a stablo komponenti je upravo tamo gdje ste ga pohranili. Ovdje mislim da se mentalni model za forme u React-u lomi, i zapravo niko nije kriv. RHF + Zod stack je odličan u onome za šta je dizajniran. Problem je u tome što smo skloni da ga koristimo nakon tačke u kojoj se njene apstrakcije podudaraju s problemom jer alternativa zahtijeva potpuno drugačiji način razmišljanja o oblicima. Ovaj članak je o toj alternativi. Da bismo to pokazali, dvaput ćemo napraviti potpuno isti obrazac u više koraka:
Sa React Hook Form + Zod povezanim na React Query za podnošenje, Sa SurveyJS, koji tretira formu kao podatke — jednostavnu JSON šemu — umjesto kao stablo komponenti.
Isti zahtjevi, ista uvjetna logika, isti API poziv na kraju. Zatim ćemo mapirati tačno šta se pomerilo, a šta je ostalo, i izložiti praktičan način da odlučite koji model treba da koristite i kada. Forma koju gradimo:
Ovaj obrazac će koristiti tok u 4 koraka: Korak 1: Detalji
Ime (obavezno), E-pošta (obavezno, važeći format).
Korak 2: Naručite
jedinična cijena, količina, poreska stopa, Izvedeno: Međuzbroj, porez, Ukupno.
Korak 3: Račun i povratne informacije
Imate li račun? (da/ne) Ako Da → korisničko ime + lozinka, oba su potrebna. Ako Ne → e-pošta je već prikupljena u koraku 1.
Ocjena zadovoljstva (1–5) Ako ≥ 4 → pitajte “Šta vam se svidjelo?” Ako je ≤ 2 → pitajte "Šta možemo poboljšati?"
Korak 4: Pregledajte
Pojavljuje se samo ako je ukupno >= 100 Konačna predaja.
Ovo nije ekstremno. Ali dovoljno je da se razotkriju arhitektonske razlike. Dio 1: Komponenta vođena (React Hook Form + Zod) Instalacija npm install react-hook-form zod @hookform/resolvers @tanstack/react-query
Zod Schema Počnimo sa Zod šemom, jer se tu obično uspostavlja oblik forme. Za prva dva koraka — lične podatke i unos narudžbe — sve je jednostavno: potrebni nizovi, brojevi sa minimumom i enum. Zanimljivi dio počinje kada pokušate izraziti uvjetna pravila.
import { z } iz "zod";
export const formSchema = z.object({ firstName: z.string().min(1, "Obavezno"), email: z.string().email("Invalid email"), cijena: z.number().min(0), količina: z.number().min(1), taxRate: z.number(), hasAccount: z. z.string().optional(), lozinka: z.string().optional(), zadovoljstvo: z.number().min(1).max(5), pozitivnaFeedback: z.string().optional(), poboljšanjeFeedback: z.string().optional(),}).superRefine((data, ctxta) => {s if (Ycdacount) => {s if (Ycdacount) (!data.username) { ctx.addIssue({ code: "custom", path: ["username"], message: "required" } } if (!data.password || data.password.length < 6) { ctx.addIssue({ code: "password", path:]); }
if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ code: "custom", path: ["positiveFeedback"], poruka: "Podijelite ono što vam se sviđa" }); }
if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ code: "custom", put:["improvementFeedback"], poruka: "Molim vas, recite nam šta da poboljšamo" }); }});
tip izvoza FormData = z.infer
Primijetite da se korisničko ime i lozinka upisuju kao opcioni() iako su uvjetno obavezni jer Zodova shema na razini tipa opisuje oblik objekta, a ne pravila koja određuju kada su polja bitna. Uslovni zahtjev mora živjeti unutar superRefine, koji se pokreće nakon što je oblik potvrđen i ima pristup cijelom objektu. To razdvajanje nije mana; to je upravo ono za šta je alat dizajniran: superRefine je mjesto gdje ide logika unakrsnih polja kada se ne može izraziti u samoj strukturi šeme. Ono što je takođe primetno ovde je ono što ova šema ne izražava. Nema koncept stranica, nema koncepta koja polja su vidljiva u kojoj tački, niti koncept navigacije. Sve će to živjeti negdje drugdje. Komponenta obrasca
import { useForm, useWatch } iz "react-hook-form";import { zodResolver } iz "@hookform/resolvers/zod";import { useMutation } iz "@tanstack/react-query";import { useState, useMemo } iz "react";import { formSchema.}
const STEPS = ["detalji", "narudžba", "račun", "pregled"];
type OrderPayload = FormData & { subtotal: broj; porez: broj; ukupno: broj };
eksport funkcija RHFMultiStepForm() { const [korak, setStep] = useState(0);
const mutation = useMutation({ mutationFn: async (korisno opterećenje: OrderPayload) => { const res = čekaj dohvat("/api/orders", { metoda: "POST", zaglavlja: { "Content-Type": "application/json" }, tijelo: JSON.stringify (korisno opterećenje), }); if (!res.ok) throw new Error("Neuspjelo slanje"); return res.json(); }, });
const { register, control, handleSubmit, formState: { errors }, } = useForm
return (
);}Pogledajte Pen SurveyJS-03-RHF [forked] by sixthextinction. Ovdje se dosta toga dešava i vrijedi usporiti da primijetite gdje su stvari završile.
Izvedene vrijednosti - međuzbroj, porez, ukupno - izračunavaju se u komponenti putem useWatch i useMemo jer zavise od živih vrijednosti polja i ne postoji drugo prirodno mjesto za njih. Pravila vidljivosti za korisničko ime, lozinku, pozitivneFeedback i poboljšaneFeedback žive u JSX-u kao inline uvjeti. Logika preskakanja koraka — stranica za pregled koja se pojavljuje samo kada je ukupno >= 100 — ugrađena je u varijablu showSubmit i uvjet renderiranja u koraku 3. Sama navigacija je samo useState brojač koji ručno povećavamo. React Query upravlja ponovnim pokušajima, keširanjem i poništavanjem. Obrazac samo poziva mutation.mutate sa potvrđenim podacima.
Ništa od ovoga nije pogrešno, samo po sebi. Ovo je još uvijek idiomatski React, a komponenta je prilično učinkovita zahvaljujući tome kako RHF izoluje re-rendere. Ali ako biste ovo predali nekome ko to nije napisao i zamolili ga da objasni pod kojim se uvjetima pojavljuje stranica za pregled, morali bi pratiti kroz showSubmit, stanje renderiranja u koraku 3 i logiku navigacijskog dugmeta - tri odvojena mjesta - da rekonstruišu pravilo koje je moglo biti navedeno u jednom redu. Obrazac funkcionira, da, ali ponašanje nije stvarno provjerljivo kao sistem. Mora se izvršiti mentalno. Što je još važnije, njegova promjena zahtijeva uključenje inženjera. Čak i malo podešavanje, poput prilagođavanja kada se pojavi korak pregleda, znači uređivanje komponente, ažuriranje validacije, otvaranje zahtjeva za povlačenjem, čekanje pregleda i ponovno postavljanje. Dio 2: vođen shemom (SurveyJS) Sada napravimo isti tok koristeći šemu. Instalacija npm install survey-core survey-react-ui @tanstack/react-query
SurveyJS-ov jezgro MIT-licenciran platformski nezavisan runtime engine koji pokreće SurveyJS-ovo renderiranje forme – dio do kojeg nam je ovdje stalo. Uzima JSON šemu, iz nje gradi interni model i rukuje svime što bi inače živjelo u vašoj React komponenti: procjenu izraza vidljivosti, izračunavanje izvedenih vrijednosti, upravljanje stanjem stranice, praćenje validacije i odlučivanje što znači "potpuno" s obzirom na to koje su stranice stvarno prikazane.
survey-react-uiSloj korisničkog sučelja / renderiranja koji povezuje taj model sa React-om. To je u suštini komponenta
Zajedno, oni vam daju potpuno funkcionalno, višestranično vrijeme izvođenja obrasca bez pisanja niti jedne linije toka kontrole. Sam format šeme je, kao što je već rečeno, samo JSON — bez DSL-a ili bilo čega vlasničkog. Možete ga umetnuti, uvesti iz datoteke, preuzeti iz API-ja ili pohraniti u kolonu baze podataka i hidratizirati u vrijeme izvođenja. Isti oblik, kao podaci Evo istog obrasca, ovog puta izraženog kao JSON objekat. Šema definira sve: strukturu, validaciju, pravila vidljivosti, izvedene proračune, navigaciju po stranici — i predaje je modelu koji to procjenjuje u vrijeme izvođenja. Evo kako to izgleda u potpunosti:
export const surveySchema = { title: "Tok narudžbe", showProgressBar: "top", stranice: [ { name: "detalji", elementi: [ { type: "text", name: "firstName", isRequired: true }, { type: "text", name: "email", inputType: "email", isRequired: "email", isRequired:{ text: validators:{ text: validators: }] } ] }, { naziv: "narudžba", elementi: [ { tip: "tekst", naziv: "cijena", inputType: "broj", defaultValue: 0 }, { tip: "tekst", naziv: "količina", inputType: "broj", defaultValue: 1 }, { tip: "padajući",naziv: "taxRate", defaultValue: 0,1, izbori: [ { vrijednost: 0,05, tekst: "5%" }, { vrijednost: 0,1, tekst: "10%" }, { vrijednost: 0,15, tekst: "15%" } ] }, { tip: "izraz", naziv: "podukupni iznos" {} "izraz", naziv: "porez", izraz: "{podukupni} {taxRate}" }, { tip: "izraz", naziv: "ukupno", izraz: "{podukupni} + {porez}" } ] }, { naziv: "račun", elementi: [ { tip: "radiogrupa", naziv: "hasAccountes", izbori ", {tip:"} "korisničko ime", visibleIf: "{hasAccount} = 'Da'", je Obavezno: istina }, { tip: "tekst", ime: "password", inputType: "password", visibleIf: "{hasAccount} = 'Da'", je Obavezno: istina, validatori znakova: [{ tip: "tekst: u 6"] }, { type: "rating", name: "satisfaction", rateMin: 1, rateMax: 5 }, { type: "comment", name: "positiveFeedback", visibleIf: "{satisfaction} >= 4" }, { type: "comment", name: "improvementFeedback", sa visibleIf:} "{ name: visibleIf" } "review", visibleIf: "{total} >= 100", elementi: [] } ]};
Uporedite ovo sa RHF verzijom na trenutak.
Blok superRefine koji je uvjetno zahtijevao korisničko ime i lozinku je nestao. visibleIf: "{hasAccount} = 'Da'" u kombinaciji sa isRequired: true rješava oba problema zajedno, na samom polju, gdje biste očekivali da ćete ih pronaći. Lanac useWatch + useMemo koji je izračunao međuzbroj, porez i ukupno zamijenjen je sa tri polja izraza koja se međusobno pozivaju po imenu. Stanje stranice za pregled, koje se u RHF verziji moglo rekonstruisati samo praćenjem kroz showSubmit, granu renderiranja u koraku 3. I konačno, logika navigacijskog dugmeta je jedno svojstvo visibleIf na objektu stranice.
Tu je ista logika. Samo što mu shema daje mjesto za život gdje je vidljiva izolovano, umjesto da se širi po komponenti. Također, imajte na umu da shema koristi tip: 'expression' za međuzbroj, porez i total. Izraz je samo za čitanje i koristi se uglavnom za prikaz izračunatih vrijednosti. SurveyJS također podržava tip: 'html' za statički sadržaj, ali za izračunate vrijednosti izraz je pravi izbor. Sada za React stranu. Rendering And Submission Vrlo jednostavno. Povežite onComplete sa svojim API-jem na isti način — putem useMutation ili običnog dohvaćanja:
import { useState, useEffect, useRef } iz "react";import { useMutation } iz "@tanstack/react-query";import { Model } iz "survey-core";import { Survey } iz "survey-react-ui";import "survey-core/survey-core.
export function SurveyForm() { const [model] = useState(() => new Model(surveySchema));
const mutation = useMutation({ mutationFn: async (podaci) => { const res = čekaj dohvat("/api/orders", { metoda: "POST", zaglavlja: { "Content-Type": "application/json" }, tijelo: JSON.stringify(podaci), }); if (!res.ok) throw new Error("Neuspjelo slanje"); return res.json(); }, });
const mutationRef = useRef(mutacija); mutationRef.current = mutacija; useEffect(() => { const handler = (sender) => mutationRef.current.mutate(sender.data); model.onComplete.add(handler); return () => model.onComplete.remove(handler); }, [model]); // ref izbjegava ponovnu registraciju rukovatelja pri svakom renderiranju (mutacija mijenja identitet objekta)
povratak (
<>
Pogledajte Pen SurveyJS-03-SurveyJS [forked] by sixthextinction.
onComplete se pokreće kada korisnik dođe do kraja posljednje vidljive stranice. Dakle, ako ukupan broj nikada ne prijeđe 100 i stranica za pregled je preskočena, ona se i dalje ispravno pokreće jer SurveyJS procjenjuje vidljivost prije nego što odluči šta znači "posljednja stranica". Zatim, sender.data sadrži sve odgovore zajedno sa izračunatim vrijednostima (subtotal, tax, total) kao prvoklasna polja, tako da je korisno opterećenje API-ja identično onome što je RHF verzija sastavila ručno u onSubmit. TheObrazac mutationRef je isti za kojim biste posegnuli bilo gdje kada vam je potreban stabilan obrađivač događaja preko vrijednosti koja se mijenja pri svakom renderu - ništa u vezi s SurveyJS-om.
React komponenta više uopće ne sadrži nikakvu poslovnu logiku. Nema useWatch-a, nema uslovnog JSX-a, nema brojača koraka, nema useMemo lanca, nema superRefine. React radi ono u čemu je zapravo dobar: renderira komponentu i povezuje je s API pozivom. Šta se iselilo iz reakcije?
Zabrinutost RHF Stack SurveyJS Vidljivost JSX podružnice visibleIf Izvedene vrijednosti useWatch / useMemo izraz Pravila unakrsnih polja superRefine Uslovi šeme Navigacija korak stanje Page visibleIf Lokacija pravila Distribuirano po fajlovima Centralizovano u šemi
Ono što ostaje u Reactu je izgled, stil, ožičenje podnošenja i integracija aplikacija, što će reći, stvari za koje je React zapravo dizajniran. Sve ostalo je premješteno u shemu, a budući da je shema samo JSON objekt, može se pohraniti u bazu podataka, verzionisana neovisno o vašem kodu aplikacije ili uređivana pomoću internih alata bez potrebe za implementacijom. Menadžer proizvoda koji treba da promijeni prag koji pokreće stranicu za pregled može to učiniti bez dodirivanja komponente. To je značajna operativna razlika za timove u kojima se ponašanje forme često razvija i nije uvijek vođeno inženjerima. Kada koristiti svaki pristup? Evo dobrog pravila koje mi funkcionira: zamislite da u potpunosti izbrišete obrazac. Šta biste izgubili?
Ako su u pitanju ekrani, želite forme vođene komponentama. Ako je to poslovna logika, kao što su pragovi, pravila grananja i uvjetni zahtjevi koji kodiraju stvarne odluke, želite mehanizam sheme.
Slično tome, ako se promjene koje dolaze uglavnom odnose na oznake, polja i raspored, RHF će vam dobro poslužiti. Ako se radi o uvjetima, ishodima i pravilima koje bi vaš operativni ili pravni tim možda trebao prilagoditi utorkom popodne bez podnošenja prijave, model sheme s SurveyJS je iskreniji. Ova dva pristupa zapravo nisu u konkurenciji jedan s drugim. Oni se bave različitim klasama problema, a greška koju vrijedi izbjegavati je neusklađivanje apstrakcije s težinom logike - tretiranje sistema pravila kao komponente jer je to poznato oruđe, ili posezanje za mehanizmom politike jer je obrazac narastao na tri koraka i stekao uslovno polje. Forma koju smo ovdje izgradili nalazi se blizu granice namjerno, dovoljno složena da razotkrije razliku, ali ne toliko ekstremna da se poređenje čini namještenim. Većina stvarnih formi koje su postale nezgrapne u vašoj bazi koda vjerovatno se nalaze blizu te iste granice, a pitanje je obično samo da li je neko imenovao ono što zapravo jesu. Koristite React Hook Form + Zod kada:
Obrasci su CRUD orijentisani; Logika je plitka i vođena UI; Inženjeri posjeduju svo ponašanje; Backend ostaje izvor istine.
Koristite SurveyJS kada:
Obrasci kodiraju poslovne odluke; Pravila se razvijaju nezavisno od korisničkog interfejsa; Logika mora biti vidljiva, provjerljiva ili verzionirana; Neinženjeri utiču na ponašanje; Isti obrazac mora se izvoditi na više frontendova.