Ovaj članak sponzorira SurveyJS Postoji mentalni model koji većina React programera dijeli, a da o njemu nikada ne raspravljaju naglas. Da forme uvijek trebaju biti komponente. To znači hrpu poput:
Obrazac React Hook za lokalno stanje (minimalno ponovno renderiranje, ergonomska registracija polja, imperativna interakcija). Zod za provjeru valjanosti (ispravnost unosa, provjera valjanosti granica, sigurnosno raščlanjivanje). React Query za backend: podnošenje, ponovni pokušaji, predmemorija, sinkronizacija poslužitelja itd.
A za veliku većinu obrazaca - vaše zaslone za prijavu, vaše stranice postavki, vaše CRUD modale - ovo jako dobro funkcionira. Svaki komad radi svoj posao, čisto se komponiraju i možete prijeći na dijelove svoje aplikacije koji zapravo razlikuju vaš proizvod. No s vremena na vrijeme obrazac počne gomilati stvari poput pravila vidljivosti koja ovise o ranijim odgovorima ili izvedenih vrijednosti koje se nižu kroz tri polja. Možda čak i cijele stranice koje bi se trebale preskočiti ili prikazati na temelju tekućeg ukupnog broja. Prvim uvjetom upravljate s useWatčem i inline granom, što je u redu. Zatim još jedan. Zatim posežete za superRefineom za kodiranje pravila više polja koja vaša Zod shema ne može izraziti na normalan način. Zatim, postupna navigacija počinje gubiti poslovnu logiku. U nekom trenutku pogledate što ste izgradili i shvatite da forma zapravo više nije korisničko sučelje. To je više proces odlučivanja, a stablo komponenti je upravo mjesto gdje ste ga pohranili. Ovdje mislim da se mentalni model za forme u Reactu kvari, a to zapravo nije ničija krivnja. RHF + Zod skup izvrstan je u onome za što je dizajniran. Problem je u tome što smo skloni nastaviti ga koristiti nakon točke u kojoj se njegove apstrakcije poklapaju s problemom jer alternativa zahtijeva sasvim drugačiji način razmišljanja o oblicima. Ovaj članak govori o toj alternativi. Da bismo to pokazali, dvaput ćemo izraditi točno isti obrazac u više koraka:
Uz React Hook obrazac + Zod spojen na React Query za slanje, Uz SurveyJS, koji obrazac tretira kao podatke — jednostavnu JSON shemu — umjesto stabla komponenti.
Isti zahtjevi, ista uvjetna logika, isti API poziv na kraju. Zatim ćemo mapirati točno što se pomaknulo, a što je ostalo i iznijeti praktičan način da odlučite koji model trebate koristiti i kada. Obrazac koji gradimo:
Ovaj će obrazac koristiti tijek od 4 koraka: 1. korak: pojedinosti
Ime (obavezno), Email (obavezno, važeći format).
Korak 2: Naručite
Jedinična cijena, količina, porezna stopa, Izvedeno: Međuzbroj, porez, Ukupno.
Korak 3: Račun i povratne informacije
Imate li račun? (Da/Ne) Ako je Da → korisničko ime + lozinka, potrebno je oboje. Ako Ne → e-pošta je već prikupljena u koraku 1.
Ocjena zadovoljstva (1–5) Ako je ≥ 4 → pitajte "Što vam se svidjelo?" Ako je ≤ 2 → pitajte "Što možemo poboljšati?"
Korak 4: Pregled
Pojavljuje se samo ako je ukupno >= 100 Konačna predaja.
Ovo nije ekstremno. Ali to je dovoljno za izlaganje arhitektonskih razlika. 1. dio: Pokretan komponentom (React Hook Form + Zod) Instalacija npm instaliraj react-hook-form zod @hookform/resolvers @tanstack/react-query
Zod shema Počnimo sa Zod shemom, jer se tamo obično uspostavlja oblik forme. Za prva dva koraka — osobne podatke i unos narudžbe — sve je jednostavno: potrebni nizovi, brojevi s minimumima i enum. Zanimljiv dio počinje kada pokušate izraziti uvjetna pravila.
import { z } iz "zod";
export const formSchema = z.object({ firstName: z.string().min(1, "Required"), email: z.string().email("Invalid email"), price: z.number().min(0), quantity: z.number().min(1), taxRate: z.number(), hasAccount: z.enum(["Yes", "No"]), username: z.string().optional(), lozinka: z.string().optional(), zadovoljstvo: z.number().min(1).max(5), positiveFeedback: z.string().optional(), poboljšanjaFeedback: z.string().optional(),}).superRefine((data, ctx) => { if (data.hasAccount === "Da") { if (!data.username) { ctx.addIssue({ kod: "prilagođeno", put: ["korisničko ime"], poruka: "Potrebno" });
if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ code: "custom", path: ["positiveFeedback"], message: "Molimo podijelite što vam se svidjelo" }); }
if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ kod: "prilagođeno", put:["improvementFeedback"], poruka: "Molim vas, recite nam što da poboljšamo" }); }});
tip izvoza FormData = z.infer
Primijetite da su korisničko ime i lozinka upisani kao optional() iako su uvjetno obavezni jer Zod-ova shema na razini tipa opisuje oblik objekta, a ne pravila koja određuju kada su polja bitna. Uvjetni zahtjev mora živjeti unutar superRefine, koji se pokreće nakon što se oblik potvrdi i ima pristup cijelom objektu. Ta odvojenost nije mana; to je upravo ono za što je alat dizajniran: superRefine je mjesto gdje ide logika više polja kada se ne može izraziti u samoj strukturi sheme. Ono što je također važno ovdje je ono što ova shema ne izražava. Nema pojma stranica, pojma koja su polja vidljiva na kojem mjestu, niti koncepta 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, type FormData } from "./schema";
const STEPS = ["detalji", "narudžba", "račun", "pregled"];
type OrderPayload = FormData & { subtotal: number; porez: broj; ukupno: broj };
funkcija izvoza RHFMultiStepForm() { const [korak, setStep] = useState(0);
const mutacija = useMutation({ mutationFn: async (payload: OrderPayload) => { const res = await fetch("/api/orders", { metoda: "POST", zaglavlja: { "Content-Type": "application/json" }, tijelo: JSON.stringify(payload), }); if (!res.ok) throw new Error("Failed to submit"); vrati res.json(); }, });
const { register, control, handleSubmit, formState: { errors }, } = useForm
return (
);}Pogledajte Pen SurveyJS-03-RHF [forked] od sixthextinction. Ovdje se dosta toga događa i vrijedi usporiti kako bismo primijetili gdje su stvari završile.
Izvedene vrijednosti — međuzbroj, porez, ukupni — izračunavaju se u komponenti putem useWatch i useMemo jer ovise o živim vrijednostima polja i ne postoji drugo prirodno mjesto za njih. Pravila vidljivosti za korisničko ime, lozinku, pozitivne povratne informacije i poboljšane povratne informacije žive u JSX-u kao ugrađeni uvjeti. Logika preskakanja koraka — stranica s pregledom koja se pojavljuje samo kada je ukupan broj >= 100 — ugrađena je u varijablu showSubmit i uvjet renderiranja u 3. koraku. Sama navigacija samo je useState brojač koji ručno povećavamo. React Query obrađuje ponovne pokušaje, predmemoriju i poništavanje. Obrazac samo poziva mutation.mutate s 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 izolira ponovno renderiranje. Ali ako biste ovo predali nekome tko to nije napisao i zamolili ih da vam objasne pod kojim se uvjetima pojavljuje stranica s pregledom, morali bi pratiti kroz showSubmit, uvjet renderiranja koraka 3 i logiku gumba za navigaciju - tri odvojena mjesta - kako bi rekonstruirali pravilo koje je moglo biti navedeno u jednom retku. Forma funkcionira, da, ali ponašanje zapravo nije moguće provjeriti kao sustav. Mora se izvršiti mentalno. Što je još važnije, promjena zahtijeva angažman inženjera. Čak i malo podešavanje, poput podešavanja kada se pojavi korak pregleda, znači uređivanje komponente, ažuriranje provjere valjanosti, otvaranje zahtjeva za povlačenjem, čekanje pregleda i ponovnu implementaciju. 2. dio: Upravljano shemom (SurveyJS) Sada izgradimo isti tok pomoću sheme. Instalacija npm instalirajte survey-core survey-react-ui @tanstack/react-query
survey-coreMehanizam za izvršavanje neovisan o platformi s licencom MIT-a koji pokreće SurveyJS-ovo prikazivanje obrazaca — dio koji nam je ovdje stalo. Uzima JSON shemu, iz nje gradi interni model i obrađuje sve što bi inače bilo u vašoj React komponenti: procjenu izraza vidljivosti, izračunavanje izvedenih vrijednosti, upravljanje stanjem stranice, praćenje provjere valjanosti i odlučivanje što znači "dovršeno" s obzirom na to koje su stranice stvarno prikazane.
survey-react-uiSloj sučelja/renderiranja koji povezuje taj model s Reactom. To je u biti komponenta
Zajedno vam daju potpuno funkcionalno vrijeme izvođenja obrasca s više stranica bez pisanja ijednog retka tijeka kontrole. Sam format sheme je, kao što je prije rečeno, samo JSON — bez DSL-a ili bilo čega vlasničkog. Možete ga ugraditi, uvesti iz datoteke, dohvatiti iz API-ja ili pohraniti u stupac baze podataka i hidratizirati ga tijekom izvođenja. Isti obrazac, kao podaci Evo istog oblika, ovaj put izraženog kao JSON objekt. Shema definira sve: strukturu, provjeru valjanosti, pravila vidljivosti, izvedene izračune, navigaciju stranicom — i predaje je modelu koji je procjenjuje tijekom izvođenja. Evo kako to izgleda u cijelosti:
export const surveySchema = { title: "Order Flow", showProgressBar: "top", pages: [ { name: "details", elements: [ { type: "text", name: "firstName", isRequired: true }, { type: "text", name: "email", inputType: "email", isRequired: true, validators: [{ type: "email", text: "Invalid email" }] } ] }, { name: "narudžba", elementi: [ { type: "text", name: "price", inputType: "number", defaultValue: 0 }, { type: "text", name: "quantity", inputType: "number", defaultValue: 1 }, { type: "dropdown",naziv: "porezna stopa", zadana vrijednost: 0,1, izbori: [ { vrijednost: 0,05, tekst: "5%" }, { vrijednost: 0,1, tekst: "10%" }, { vrijednost: 0,15, tekst: "15%" } ] }, { tip: "izraz", naziv: "podzbroj", izraz: "{cijena} {količina}" }, { tip: "izraz", naziv: "porez", izraz: "{subtotal} {taxRate}" }, { tip: "izraz", naziv: "ukupno", izraz: "{subtotal} + {porez}" } ] }, { naziv: "račun", elementi: [ { tip: "radiogroup", naziv: "hasAccount", izbori: ["Da", "Ne"] }, { tip: "tekst", naziv: "korisničko ime", visibleIf: "{hasAccount} = 'Da'", isRequired: true }, { type: "text", name: "password", inputType: "password", visibleIf: "{hasAccount} = 'Yes'", isRequired: true, validatori: [{ type: "text", minLength: 6, text: "Min 6 characters" }] }, { type: "ocjena", name: "zadovoljstvo", rateMin: 1, rateMax: 5 }, { type: "comment", name: "positiveFeedback", visibleIf: "{satisfaction} >= 4" }, { type: "comment", name: "improvementFeedback", visibleIf: "{satisfaction} <= 2" } ] }, { name: "review", visibleIf: "{ukupno} >= 100", elementi: [] } ]};
Usporedite ovo na trenutak s RHF verzijom.
Nestao je blok superRefine koji je uvjetno zahtijevao korisničko ime i lozinku. visibleIf: "{hasAccount} = 'Da'" u kombinaciji s isRequired: true obrađuje oba pitanja zajedno, na samom polju, gdje biste ih očekivali pronaći. Lanac useWatch + useMemo koji je izračunao međuzbroj, porez i ukupni zamijenjen je s tri izrazna polja koja se međusobno referiraju po imenu. Stanje stranice za recenziju, koje je u RHF verziji bilo moguće rekonstruirati samo praćenjem kroz showSubmit, granu prikazivanja koraka 3. I konačno, logika gumba za navigaciju jedno je svojstvo visibleIf na objektu stranice.
Tu je ista logika. Samo što mu shema daje mjesto za život gdje je vidljiv u izolaciji, umjesto da se širi po komponenti. Također imajte na umu da shema koristi type: 'expression' za međuzbroj, porez i ukupni iznos. Izraz je samo za čitanje i koristi se uglavnom za prikaz izračunatih vrijednosti. SurveyJS također podržava type: 'html' za statički sadržaj, ali za izračunate vrijednosti izraz je pravi izbor. Sada za React stranu. Prikaz i podnošenje Vrlo jednostavno. Spojite onComplete na svoj API na isti način — putem useMutation ili običnog dohvaćanja:
uvoz { useState, useEffect, useRef } iz "react"; uvoz { useMutation } iz "@tanstack/react-query"; uvoz { Model } iz "survey-core"; uvoz { Anketa } iz "survey-react-ui"; uvoz "survey-core/survey-core.css";
funkcija izvoza AnketaForm() { const [model] = useState(() => novi Model(anketaSchema));
const mutacija = useMutation({ mutationFn: async (podaci) => { const res = await fetch("/api/orders", { metoda: "POST", zaglavlja: { "Content-Type": "application/json" }, tijelo: JSON.stringify(podaci), }); if (!res.ok) throw new Error("Failed to submit"); vrati res.json(); }, });
const mutationRef = useRef(mutacija); mutationRef.current = mutacija; useEffect(() => { const handler = (pošiljatelj) => mutationRef.current.mutate(sender.data); model.onComplete.add(handler); return () => model.onComplete.remove(handler); }, [model]); // ref izbjegava ponovnu registraciju rukovatelja svakim renderiranjem (promjene identiteta objekta mutacije)
povratak (
<>
Pogledajte Pen SurveyJS-03-SurveyJS [forked] od sixthextinction.
onComplete se aktivira kada korisnik dođe do kraja zadnje vidljive stranice. Dakle, ako zbroj nikada ne prijeđe 100 i stranica za recenziju je preskočena, i dalje se aktivira ispravno jer SurveyJS procjenjuje vidljivost prije nego što odluči što znači "posljednja stranica". Zatim, sender.data sadrži sve odgovore zajedno s izračunatim vrijednostima (subtotal, tax, total) kao polja prve klase, tako da je korisni teret API-ja identičan onome što je RHF verzija sastavila ručno u onSubmit. ThemutationRef obrazac isti je onaj za kojim biste posegnuli bilo gdje gdje vam je potreban stabilan rukovatelj događajima iznad vrijednosti koja se mijenja pri svakom renderiranju — nema ništa specifično za SurveyJS.
Komponenta React više ne sadrži nikakvu poslovnu logiku. Ne postoji useWatch, nema uvjetni JSX, nema brojač koraka, nema useMemo lanac, nema superRefine. React radi ono u čemu je zapravo dobar: renderira komponentu i povezuje je s API pozivom. Što se pomaknulo iz Reacta?
Zabrinutost RHF snop SurveyJS Vidljivost JSX grane vidljivIf Izvedene vrijednosti useWatch / useMemo izražavanje Pravila između polja superPročistiti Uvjeti sheme Navigacija stanje koraka Stranica vidljivaIf Lokacija pravila Distribuirano po datotekama Centralizirano u shemi
Ono što ostaje u Reactu je izgled, stil, ožičenje za podnošenje i integracija aplikacije, što će reći, stvari za koje je React zapravo dizajniran. Sve ostalo premješteno je u shemu, a budući da je shema samo JSON objekt, može se pohraniti u bazu podataka, imati verziju neovisno o kodu vaše aplikacije ili uređivati pomoću internog alata bez potrebe za implementacijom. Voditelj proizvoda koji treba promijeniti 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 ne pokreću ga uvijek inženjeri. Kada koristiti svaki pristup? Evo dobrog praktičnog pravila koje mi funkcionira: zamislite da potpuno izbrišete obrazac. Što biste izgubili?
Ako se radi o zaslonima, želite obrasce vođene komponentama. Ako je 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 vam dolaze uglavnom odnose na oznake, polja i izgled, 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 u utorak poslijepodne bez podnošenja prijave, model sheme sa SurveyJS-om pošteniji je. Ova dva pristupa zapravo nisu u konkurenciji jedan s drugim. Oni se bave različitim klasama problema, a pogreška koju vrijedi izbjegavati je neusklađivanje apstrakcije s težinom logike - tretiranje sustava pravila kao komponente jer je to poznati alat ili posezanje za motorom politike jer je obrazac narastao na tri koraka i dobio uvjetno polje. Oblik koji smo ovdje izgradili nalazi se blizu granice namjerno, dovoljno složen da izloži razliku, ali ne toliko ekstreman da se usporedba čini namještenom. Većina stvarnih oblika koji su postali nezgrapni u vašoj bazi koda vjerojatno se nalaze blizu te iste granice, a pitanje je obično samo je li itko imenovao što oni zapravo jesu. Koristite React Hook Form + Zod kada:
Obrasci su CRUD-orijentirani; Logika je plitka i vođena korisničkim sučeljem; Inženjeri posjeduju svo ponašanje; Backend ostaje izvor istine.
Koristite SurveyJS kada:
Obrasci kodiraju poslovne odluke; Pravila se razvijaju neovisno o korisničkom sučelju; Logika mora biti vidljiva, provjerljiva ili verzirana; Neinženjeri utječu na ponašanje; Isti obrazac mora se izvoditi na više sučelja.