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({ ratkaisija: zodResolver(formSchema), oletusarvot: { hinta: 0, määrä: 1, veroprosentti: 0.1, tyytyväisyys: 3, hasAccount: "Ei", }, }); const price = useWatch({ ohjausobjekti, nimi: "hinta" }); const määrä = useWatch({ ohjausobjekti, nimi: "määrä" }); const taxRate = useWatch({ ohjausobjekti, nimi: "taxRate" }); const hasAccount = useWatch({ ohjausobjekti, nimi: "hasAccount" }); const satisfaction = useWatch({ ohjausobjekti, nimi: "tyytyväisyys" }); const välisumma = useMemo(() => (hinta ?? 0) * (määrä ?? 1), [hinta, määrä]); const tax = useMemo(() => välisumma * (taxRate ?? 0), [välisumma, veroprosentti]); const yhteensä = useMemo(() => välisumma + vero, [välisumma, vero]); const onSubmit = (tiedot: FormData) => mutation.mutate({ ...data, välisumma, vero, yhteensä }); const showSubmit = (vaihe === 2 && yhteensä < 100) || (vaihe === 3 && yhteensä >= 100)

return (

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

{vaihe === 1 && ( <>

Välisumma: {subtotal}
Vero: {tax}
Yhteensä: {total}
)}

{step === 2 && ( <>

{hasAccount === "Kyllä" && ( <> )}

{tyytyväisyys >= 4 && ( )}

{tyytyväisyys <= 2 && (