Tämä artikkeli on SurveyJS:n sponsoroima On olemassa mentaalinen malli, jonka useimmat React-kehittäjät jakavat keskustelematta siitä koskaan ääneen. Että lomakkeiden oletetaan aina olevan komponentteja. Tämä tarkoittaa pinoa, kuten:
React Hook Form paikallista tilaa varten (minimaalinen uudelleenrenderöinti, ergonominen kentän rekisteröinti, pakollinen vuorovaikutus). Zod vahvistusta varten (syötteen oikeellisuus, rajatarkistus, tyyppiturvallinen jäsennys). React Query for backend: lähetys, uudelleenyritykset, välimuisti, palvelimen synkronointi ja niin edelleen.
Ja suurimmalla osalla lomakkeista – kirjautumisnäytöistäsi, asetussivuistasi, CRUD-modaaleistasi – tämä toimii todella hyvin. Jokainen kappale tekee tehtävänsä, ne sommittelevat siististi, ja voit siirtyä sovelluksesi osiin, jotka todella erottavat tuotteesi. Mutta silloin tällöin lomake alkaa kerätä asioita, kuten näkyvyyssääntöjä, jotka riippuvat aikaisemmista vastauksista, tai johdettuja arvoja, jotka jatkuvat kolmen kentän läpi. Ehkä jopa kokonaisia sivuja, jotka pitäisi ohittaa tai näyttää juoksevan kokonaissumman perusteella. Käsittelet ensimmäistä ehdollista useWatchilla ja rivihaaralla, mikä on hyvä. Sitten toinen. Sitten tavoitat superRefinen koodataksesi kenttien välisiä sääntöjä, joita Zod-skeemasi ei voi ilmaista normaalilla tavalla. Sitten askelnavigointi alkaa vuotaa liiketoimintalogiikkaa. Jossain vaiheessa katsot mitä olet rakentanut ja huomaat, että lomake ei ole enää käyttöliittymä. Se on enemmän päätösprosessi, ja komponenttipuu on juuri siellä, missä sen tallensit. Tässä mielestäni Reactin muotojen mentaalinen malli hajoaa, eikä se todellakaan ole kenenkään vika. RHF + Zod -pino on erinomainen siinä, mihin se on suunniteltu. Ongelmana on, että meillä on tapana jatkaa sen käyttöä sen pisteen jälkeen, jossa sen abstraktiot vastaavat ongelmaa, koska vaihtoehto vaatii erilaista ajattelutapaa muodoista kokonaan. Tämä artikkeli käsittelee tätä vaihtoehtoa. Tämän osoittamiseksi rakennamme täsmälleen saman monivaiheisen lomakkeen kahdesti:
Kun React Hook Form + Zod on kytketty React Query -kyselyyn lähettämistä varten, SurveyJS:llä, joka käsittelee lomaketta datana – yksinkertaisena JSON-skeemana – eikä komponenttipuuna.
Samat vaatimukset, sama ehdollinen logiikka, sama API-kutsu lopussa. Sitten kartoitamme tarkalleen, mikä liikkui ja mikä jäi, ja laadimme käytännöllisen tavan päättää, mitä mallia sinun tulisi käyttää ja milloin. Rakentamamme muoto:
Tämä lomake käyttää 4-vaiheista kulkua: Vaihe 1: Tiedot
Etunimi (pakollinen), Sähköposti (pakollinen, kelvollinen muoto).
Vaihe 2: Tilaa
Yksikköhinta, määrä, veroprosentti, Johdettu: Välisumma, vero, Yhteensä.
Vaihe 3: Tili ja palaute
Onko sinulla tili? (Kyllä/Ei) Jos Kyllä → käyttäjätunnus + salasana, molemmat vaaditaan. Jos ei → sähköposti on jo kerätty vaiheessa 1.
Tyytyväisyysluokitus (1–5) Jos ≥ 4 → kysy "Mistä pidit?" Jos ≤ 2 → kysy "Mitä voimme parantaa?"
Vaihe 4: Tarkista
Näkyy vain, jos yhteensä >= 100 Lopullinen lähetys.
Tämä ei ole äärimmäistä. Mutta se riittää paljastamaan arkkitehtoniset erot. Osa 1: Komponenttikäyttöinen (React Hook Form + Zod) Asennus npm asenna react-hook-form zod @hookform/resolvers @tanstack/react-query
Zod Schema Aloitetaan Zod-skeemasta, koska siellä muodon muoto yleensä vakiintuu. Kahdessa ensimmäisessä vaiheessa – henkilökohtaiset tiedot ja tilaussyötteet – kaikki on suoraviivaista: vaaditut merkkijonot, numerot minimiin ja enum. Mielenkiintoinen osa alkaa, kun yrität ilmaista ehdolliset säännöt.
tuo { z } "zodista";
export const formSchema = z.object({ etunimi: z.string().min(1, "pakollinen"), sähköpostiosoite: z.string().email("Virheellinen sähköpostiosoite"), hinta: z.numero().min(0), määrä: z.numero().min(1), taxRate: z.number(), hasAccount: ["],enum",(["].esum z.string().optional(), salasana: z.string().valinnainen(), tyytyväisyys: z.numero().min(1).max(5), positiivinenPalaute: z.string().optional(), parannusPalaute: z.string().optional(),}).superRefine((data, ctxda) =.=.ha { if (ctxda) =. (!data.username) { ctx.addIssue({ koodi: "mukautettu", polku: ["käyttäjänimi"], viesti: "pakollinen" }); } if (!data.password || data.password.length < 6) { ctx.addIssue({ code: "customword" , polku:]); } }
if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ koodi: "custom", polku: ["positiivinenFeedback"], viesti: "Jaa mistä pidit" }); }
if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ code: "custom", polku:["improvementFeedback"], viesti: "Kerro meille, mitä pitäisi parantaa" }); }});
vientityyppi FormData = z.infer
Huomaa, että käyttäjätunnus ja salasana kirjoitetaan valinnaisina()-muotoisina, vaikka ne ovat ehdollisesti pakollisia, koska Zodin tyyppitason skeema kuvaa objektin muotoa, ei kenttien merkitystä sääteleviä sääntöjä. Ehdollisen vaatimuksen on oltava superRefinessä, joka suoritetaan sen jälkeen, kun muoto on validoitu ja jolla on pääsy koko objektiin. Tämä erottaminen ei ole virhe; sitä varten työkalu on suunniteltu: superRefine on paikka, jossa kenttien välinen logiikka menee, kun sitä ei voida ilmaista itse skeemarakenteessa. Huomionarvoista tässä on myös se, mitä tämä skeema ei ilmaise. Siinä ei ole käsitettä sivuista, käsitettä siitä, mitkä kentät ovat näkyvissä missä vaiheessa, eikä käsitettä navigoinnista. Kaikki tämä asuu jossain muualla. Muotokomponentti
tuonti { useForm, useWatch } osoitteesta "react-hook-form";tuo { zodResolver } osoitteesta "@hookform/resolvers/zod";tuo { useMutation } osoitteesta "@tanstack/react-query";tuo { useState, useMemo } "react":sta, kirjoita { formSchema; kirjoita FormSchema;
const STEPS = ["tiedot", "tilaus", "tili", "arvostelu"];
type OrderPayload = FormData & { välisumma: numero; vero: numero; yhteensä: numero };
vientifunktio RHFMultiStepForm() { const [vaihe, setStep] = useState(0);
const mutaatio = useMutation({ mutationFn: async (hyötykuorma: OrderPayload) => { const res = odota fetch("/api/tilaukset", { menetelmä: "POST", otsikot: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error("Lähetys epäonnistui"); return res.json(); }, });
const { register, control, handleSubmit, formState: { errors }, } = useForm
return (
);}Katso Pen SurveyJS-03-RHF [forked] kuudella extinctionilla. Täällä tapahtuu melko paljon, ja kannattaa hidastaa vauhtia huomatakseen, mihin asiat päätyivät.
Johdetut arvot - välisumma, vero, summa - lasketaan komponentissa useWatchin ja useMemon kautta, koska ne riippuvat reaaliaikaisista kenttäarvoista eikä niille ole muuta luonnollista paikkaa. Käyttäjätunnuksen, salasanan, positiivisenpalautteen ja parannuspalautteen näkyvyyssäännöt näkyvät JSX:ssä upotettuina ehdollisina. Vaiheen ohituslogiikka – tarkistussivu tulee näkyviin vain, kun yhteensä >= 100 – on upotettu showSubmit-muuttujaan ja hahmonnusehtoon vaiheessa 3. Navigointi itsessään on vain useState-laskuri, jota lisäämme manuaalisesti. React Query käsittelee uudelleenyritykset, välimuistin tallentamisen ja mitätöinnin. Lomake kutsuu vain mutation.mutate validoiduilla tiedoilla.
Mikään näistä ei sinänsä ole väärin. Tämä on edelleen idiomaattinen React, ja komponentti on melko suorituskykyinen, koska RHF eristää uudelleen renderöinnit. Mutta jos antaisit tämän jollekulle, joka ei ole kirjoittanut sitä, ja pyytäisit häntä selittämään, millaisissa olosuhteissa arvostelusivu näkyy, hänen olisi jäljitettävä showSubmit, vaiheen 3 renderöintiehto ja navigointipainikkeen logiikka – kolme erillistä paikkaa – rekonstruoidakseen säännön, joka olisi voitu ilmaista yhdellä rivillä. Lomake toimii, kyllä, mutta käyttäytyminen ei ole todella tarkastettavissa järjestelmänä. Se on suoritettava henkisesti. Vielä tärkeämpää on, että sen muuttaminen vaatii insinöörityötä. Pienikin säätö, kuten tarkistusvaiheen ilmestymisen säätäminen, tarkoittaa komponentin muokkaamista, validoinnin päivittämistä, vetopyynnön avaamista, tarkistuksen odottamista ja uudelleen käyttöönottoa. Osa 2: Schema-Driven (SurveyJS) Rakennetaan nyt sama kulku kaavion avulla. Asennus npm asenna survey-core survey-react-ui @tanstack/react-query
survey-core MIT-lisensoitu alustasta riippumaton ajonaikainen moottori, joka toimii SurveyJS:n lomakkeen renderöinnissä – osa, josta välitämme. Se ottaa JSON-skeeman, rakentaa siitä sisäisen mallin ja käsittelee kaikkea, mikä muuten olisi React-komponentissasi: näkyvyyslausekkeiden arvioinnin, johdettujen arvojen laskemisen, sivun tilan hallinnan, vahvistuksen seurannan ja sen päättämisen, mitä "valmis" tarkoittaa, kun otetaan huomioon, mitkä sivut todella näytettiin.
survey-react-uiKäyttöliittymä/renderöintikerros, joka yhdistää mallin Reactiin. Se on pohjimmiltaan
Yhdessä ne antavat sinulle täysin toimivan, monisivuisen lomakkeen suoritusajan kirjoittamatta yhtään ohjausvirtaa. Itse skeemamuoto on, kuten aiemmin mainittiin, vain JSON - ei DSL:ää tai mitään omaa. Voit upottaa sen, tuoda sen tiedostosta, hakea sen API:sta tai tallentaa sen tietokantararakkeeseen ja hydrata sen ajon aikana. Sama muoto, kuin data Tässä on sama muoto, tällä kertaa ilmaistuna JSON-objektina. Kaava määrittelee kaiken: rakenteen, validoinnin, näkyvyyssäännöt, johdetut laskelmat, sivulla navigoinnin – ja antaa sen mallille, joka arvioi sen ajon aikana. Tältä se näyttää kokonaisuudessaan:
export const surveySchema = { title: "Order Flow", showProgressBar: "top", sivut: [ { nimi: "tiedot", elementit: [ { type: "text", name: "firstName", isRequired: true }, { type: "text", name: "email", inputType: In "email", isRequired: true, email}, isRequired: true, email }, ] }, { nimi: "tilaus", elementit: [ { type: "text", name: "price", inputType: "number", defaultValue: 0 }, { type: "text", name: "quantity", inputType: "number", defaultValue: 1 }, { type: "pudotusvalikko",nimi: "taxRate", oletusarvo: 0.1, valinnat: [ { arvo: 0.05, teksti: "5%" }, { arvo: 0.1, teksti: "10%" }, { arvo: 0.15, teksti: "15%" } ] }, { type: "expression", nimi: "välisumma}", lauseke: "nt{price}", tyyppi "lauseke", nimi: "tax", lauseke: "{subtotal} {taxRate}" }, { type: "expression", nimi: "yhteensä", lauseke: "{subtotal} + {tax}" } ] }, { nimi: "tili", elementit: [ { type: "radiogroup", nimi: "hasAccount", nimi: "hasAccount", tyyppi ", "":"], valinnat: "[:"] "käyttäjänimi", näkyväIf: "{hasAccount} = 'Kyllä'", isRequired: true }, { type: "text", nimi: "salasana", inputType: "salasana", näkyväIf: "{hasAccount} = 'Kyllä'", isPakollinen: tosi, validaattorit: [{ tyyppi: "teksti", minPituus 6 merkki: ] tyyppi: "arvio", nimi: "tyytyväisyys", rateMin: 1, rateMax: 5 }, { tyyppi: "kommentti", nimi: "positiivinenFeedback", näkyväJos: "{tyytyväisyys} >= 4" }, { tyyppi: "kommentti", nimi: "parannusFeedback", näkyväJos: "{nimi = ]tyytyväisyys"}", näkyvissäIf: "{total} >= 100", elementit: [] } ]};
Vertaa tätä hetkeksi RHF-versioon.
Käyttäjätunnusta ja salasanaa ehdollisesti vaatinut superRefine-lohko on poissa. näkyvissäIf: "{hasAccount} = 'Kyllä'" yhdistettynä isRequired:iin: true käsittelee molemmat huolenaiheet yhdessä, itse kentässä, josta niiden odotetaan löytyvän. UseWatch + useMemo -ketju, joka laski välisumman, veron ja summan, korvataan kolmella lausekekentällä, jotka viittaavat toisiinsa nimellä. Tarkastelusivun ehto, joka RHF-versiossa oli rekonstruoitavissa vain jäljittämällä showSubmitin kautta, vaiheen 3 renderöintihaara. Ja lopuksi, navigointipainikkeen logiikka on yksi näkyvissäIf-ominaisuus sivuobjektissa.
Sama logiikka on olemassa. Se on vain, että skeema antaa sille asuinpaikan, jossa se näkyy erillään sen sijaan, että se on hajallaan komponentin yli. Huomaa myös, että skeema käyttää tyyppiä: 'expression' välisummalle, verolle ja summalle. Lauseke on vain luku -tilassa ja sitä käytetään pääasiassa laskettujen arvojen näyttämiseen. SurveyJS tukee myös tyyppiä: 'html' staattiselle sisällölle, mutta lasketuille arvoille lauseke on oikea valinta. Nyt React-puolelle. Renderöinti ja lähettäminen Hyvin yksinkertainen. Yhdistä onComplete API-liittymään samalla tavalla – useMutationin tai plain fetchin kautta:
tuonti { useState, useEffect, useRef } kohteesta "react";tuo { useMutation } kohteesta "@tanstack/react-query";tuo { malli } kohteesta "survey-core";tuo { Survey } from "survey-react-ui";tuo "survey-core/survey-core".
vientifunktio SurveyForm() { const [malli] = useState(() => new Model(surveySchema));
const mutaatio = useMutation({ mutationFn: async (data) => { const res = odota fetch("/api/tilaukset", { menetelmä: "POST", otsikot: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) throw new Error("Lähetys epäonnistui"); return res.json(); }, });
const mutaatioRef = useRef(mutaatio); mutationRef.current = mutaatio; useEffect(() => { const handler = (lähettäjä) => mutaatioRef.current.mutate(sender.data); model.onComplete.add(käsittelijä); return () => model.onComplete.remove(käsittelijä); }, [malli]); // ref välttää käsittelijän uudelleenrekisteröinnin joka renderöinnissä (mutaatioobjektin identiteetti muuttuu)
paluu (
<>
Katso Pen SurveyJS-03-SurveyJS [haarukka] kuuden extinctionin mukaan.
onComplete käynnistyy, kun käyttäjä saapuu viimeisen näkyvän sivun loppuun. Joten jos kokonaismäärä ei koskaan ylitä 100:aa ja arvostelusivu ohitetaan, se käynnistyy silti oikein, koska SurveyJS arvioi näkyvyyden ennen kuin päättää, mitä "viimeinen sivu" tarkoittaa. Sender.data sisältää sitten kaikki vastaukset sekä lasketut arvot (välisumma, vero, summa) ensiluokkaisina kenttinä, joten API-hyötykuorma on identtinen RHF-version kanssa, joka on koottu manuaalisesti onSubmitissa. ThemutationRef-kuvio on sama, johon tavoitat kaikkialla, missä tarvitset vakaan tapahtumakäsittelijän jokaisella renderöinnillä muuttuvan arvon yläpuolella – siinä ei ole mitään SurveyJS-spesifistä.
React-komponentti ei enää sisällä lainkaan liiketoimintalogiikkaa. Ei ole useWatchia, ei ehdollista JSX:ää, ei askellaskuria, ei useMemo-ketjua, ei superRefineä. React tekee sitä, missä se todella on hyvä: renderöi komponentin ja kytkee sen API-kutsuun. Mikä siirtyi pois reagoinnista?
Huoli RHF-pino KyselyJS Näkyvyys JSX-konttorit näkyvä Jos Johdetut arvot useWatch / useMemo ilmaisua Cross-field säännöt superrefine Kaavioehdot Navigointi vaiheen tila Sivu näkyvissäJos Säännön sijainti Jaettu tiedostoille Keskitetty skeemaan
Reactiin jää asettelu, tyyli, lähetysjohdotukset ja sovellusten integrointi, toisin sanoen asiat, joihin React on todella suunniteltu. Kaikki muu siirrettiin skeemaan, ja koska skeema on vain JSON-objekti, se voidaan tallentaa tietokantaan, versioida sovelluskoodista riippumatta tai muokata sisäisten työkalujen avulla ilman käyttöönottoa. Tuotepäällikkö, jonka on muutettava arvostelusivun käynnistävää kynnystä, voi tehdä sen koskematta komponenttiin. Tämä on merkityksellinen toiminnallinen ero tiimeille, joissa muotokäyttäytyminen muuttuu usein eivätkä aina ole insinöörien ohjaama. Milloin käyttää kutakin lähestymistapaa? Tässä on hyvä nyrkkisääntö, joka toimii minulle: kuvittele poistavasi lomakkeen kokonaan. Mitä menettäisit?
Jos se on näyttöjä, haluat komponenttipohjaisia lomakkeita. Jos kyseessä on liiketoimintalogiikka, kuten kynnykset, haarautumissäännöt ja ehdolliset vaatimukset, jotka koodaavat todellisia päätöksiä, haluat skeemamoottorin.
Vastaavasti, jos edessäsi olevat muutokset koskevat enimmäkseen tarroja, kenttiä ja asettelua, RHF palvelee sinua hyvin. Jos kyse on ehdoista, tuloksista ja säännöistä, joita operaattorisi tai lakitiimisi saattaa joutua muuttamaan tiistai-iltapäivänä ilman lippua, SurveyJS:n mallimalli sopii rehellisemmin. Nämä kaksi lähestymistapaa eivät todellakaan kilpaile keskenään. Ne käsittelevät eri luokkia ongelmia, ja välttämisen arvoinen virhe on se, että abstraktio ei sovi yhteen logiikan painon kanssa – sääntöjärjestelmän käsitteleminen komponenttina, koska se on tuttu työkalu, tai politiikkamoottorin tavoittaminen, koska lomake kasvoi kolmeen vaiheeseen ja sai ehdollisen kentän. Täällä rakentamamme muoto on tarkoituksella lähellä rajaa, tarpeeksi monimutkainen paljastamaan eron, mutta ei niin äärimmäinen, että vertailu tuntuu vääristellyltä. Useimmat todelliset muodot, jotka ovat tulleet raskaaksi koodikannassasi, sijaitsevat todennäköisesti lähellä samaa rajaa, ja kysymys on yleensä vain siitä, onko kukaan nimennyt, mitä ne todellisuudessa ovat. Käytä React Hook Form + Zod, kun:
Lomakkeet ovat CRUD-suuntautuneita; Logiikka on matala ja käyttöliittymäohjattu; Insinöörit omistavat kaiken käyttäytymisen; Tausta on edelleen totuuden lähde.
Käytä SurveyJS:ää, kun:
Lomakkeet koodaavat liiketoimintapäätöksiä; Säännöt kehittyvät käyttöliittymästä riippumatta; Logiikan on oltava näkyvää, tarkastettavaa tai versioitua; Ei-insinöörit vaikuttavat käyttäytymiseen; Saman lomakkeen on käytettävä useissa käyttöliittymissä.