Hierdie artikel word geborg deur SurveyJS Daar is 'n verstandelike model wat die meeste React-ontwikkelaars deel sonder om dit ooit hardop te bespreek. Dat vorms altyd veronderstel is om komponente te wees. Dit beteken 'n stapel soos:

Reageer-haakvorm vir plaaslike staat (minimale herlewerings, ergonomiese veldregistrasie, noodsaaklike interaksie). Zod vir validering (invoer korrektheid, grens validering, tipe-veilige ontleding). Reageer navraag vir backend: indiening, herproberings, cache, bedienersinkronisering, ensovoorts.

En vir die oorgrote meerderheid vorms - jou aanmeldskerms, jou instellingsbladsye, jou CRUD-modale - werk dit baie goed. Elke stuk doen sy werk, hulle komponeer skoon, en jy kan aanbeweeg na die dele van jou toepassing wat jou produk eintlik onderskei. Maar kort-kort begin 'n vorm dinge ophoop soos sigbaarheidsreëls wat afhang van vroeëre antwoorde, of afgeleide waardes wat deur drie velde val. Miskien selfs hele bladsye wat op grond van 'n lopende totaal oorgeslaan of gewys moet word. Jy hanteer die eerste voorwaardelike met 'n useWatch en 'n inlyn-tak, wat goed is. Dan nog een. Dan gryp jy na superRefine om kruisveldreëls te enkodeer wat jou Zod-skema nie op die normale manier kan uitdruk nie. Dan begin stapnavigasie besigheidslogika uitlek. Op 'n stadium kyk jy na wat jy gebou het en besef dat die vorm nie regtig meer UI is nie. Dit is meer 'n besluitproses, en die komponentboom is net waar jy dit toevallig gestoor het. Dit is waar ek dink die verstandelike model vir vorms in React breek, en dit is regtig niemand se skuld nie. Die RHF + Zod-stapel is uitstekend waarvoor dit ontwerp is. Die probleem is dat ons geneig is om dit aan te hou gebruik verby die punt waar die abstraksies daarvan ooreenstem met die probleem, want die alternatief vereis 'n heeltemal ander manier van dink oor vorms. Hierdie artikel handel oor daardie alternatief. Om dit te wys, sal ons presies dieselfde multi-stap vorm twee keer bou:

Met React Hook Form + Zod bedraad na React Query vir indiening, Met SurveyJS, wat 'n vorm as data behandel - 'n eenvoudige JSON-skema - eerder as 'n komponentboom.

Dieselfde vereistes, dieselfde voorwaardelike logika, dieselfde API-oproep aan die einde. Dan sal ons presies karteer wat beweeg en wat gebly het, en 'n praktiese manier uiteensit om te besluit watter model jy moet gebruik, en wanneer. Die vorm wat ons bou:

Hierdie vorm sal 'n 4-stap vloei gebruik: Stap 1: Besonderhede

Voornaam (vereis), E-pos (vereis, geldige formaat).

Stap 2: Bestel

Eenheidsprys, Hoeveelheid, Belastingkoers, Afgelei: Subtotaal, Belasting, Totaal.

Stap 3: Rekening en terugvoer

Het jy 'n rekening? (Ja/Nee) Indien Ja → gebruikersnaam + wagwoord, beide vereis. Indien Nee → e-pos reeds in stap 1 ingesamel.

Tevredenheidgradering (1–5) As ≥ 4 → vra “Waarvan het jy gehou?” As ≤ 2 → vra “Wat kan ons verbeter?”

Stap 4: Hersien

Verskyn slegs as totaal >= 100 Finale voorlegging.

Dit is nie ekstreem nie. Maar dit is genoeg om argitektoniese verskille bloot te lê. Deel 1: Komponentgedrewe (Reageerhaakvorm + Zod) Installasie npm installeer react-hook-form zod @hookform/resolvers @tanstack/react-query

Zod Skema Kom ons begin met die Zod-skema, want dit is gewoonlik waar die vorm van die vorm vasgestel word. Vir die eerste twee stappe - persoonlike besonderhede en bestelinvoere - is alles eenvoudig: vereiste stringe, nommers met minimums en 'n enum. Die interessante deel begin wanneer jy probeer om die voorwaardelike reëls uit te druk.

invoer { z } vanaf "zod";

uitvoer const formSkema = z.object({ voornaam: z.string().min(1, "Vereis"), e-pos: z.string().email("Ongeldige e-pos"), prys: z.number().min(0), hoeveelheid: z.number().min(1), belastingtarief: z.number(), hetRekening: z.enum(No"),(natuurlik), gebruikernaam([".Yes), op gebruikernaam([".Yes)." wagwoord: z.string().opsioneel(), bevrediging: z.number().min(1).maks(5), positieweTerugvoer: z.string().opsioneel(), verbeteringTerugvoer: z.string().opsioneel(),}).superRefine((data, ctx) => { if (data.hasRekening ===) "Ja" (!) ctx.addIssue({ kode: "pasgemaak", pad: ["gebruikersnaam"], boodskap: "Vereis" } } if (!data.wagwoord || data.wagwoord.length < 6) { ctx.addIssue({ kode: "pasmaak", pad: ["wagwoord"], boodskap: "Min 6 karakters"});

if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ kode: "custom", pad: ["positieweTerugvoer"], boodskap: "Deel asseblief waarvan jy gehou het" }); }

if (data.satisfaction <= 2 && !data.improvementTerugvoer) { ctx.addIssue({ kode: "custom", pad:["verbeteringTerugvoer"], boodskap: "Vertel ons asseblief wat om te verbeter" }); }});

uitvoer tipe FormData = z.infer;

Let daarop dat gebruikersnaam en wagwoord as opsioneel () getik word, alhoewel dit voorwaardelik vereis word omdat Zod se tipe-vlak-skema die vorm van die voorwerp beskryf, nie die reëls wat bepaal wanneer velde saak maak nie. Die voorwaardelike vereiste moet binne superRefine leef, wat loop nadat die vorm bekragtig is en toegang tot die volle voorwerp het. Daardie skeiding is nie 'n gebrek nie; dit is net waarvoor die instrument ontwerp is: superRefine is waar kruisveldlogika gaan wanneer dit nie in die skemastruktuur self uitgedruk kan word nie. Wat ook hier opvallend is, is wat hierdie skema nie uitdruk nie. Dit het geen konsep van bladsye nie, geen konsep van watter velde op watter punt sigbaar is nie, en geen konsep van navigasie nie. Dit alles sal iewers anders woon. Vormkomponent

invoer { useForm, useWatch } vanaf "react-hook-form"; voer { zodResolver } van "@hookform/resolvers/zod" in; voer { useMutation } in vanaf "@tanstack/react-query"; voer { useState, useMemo } in vanaf "react"; voer {form.schema in" in, tik vorm./skema };

const STEPS = ["besonderhede", "bestelling", "rekening", "resensie"];

tipe OrderPayload = FormData & { subtotaal: getal; belasting: nommer; totaal: getal };

uitvoerfunksie RHFMultiStepForm() { const [stap, setStep] = useState(0);

const mutation = useMutation({ mutationFn: async (payload: OrderPayload) => { const res = wag op haal("/api/orders", { metode: "POS", headers: { "Content-Type": "application/json" }, liggaam: JSON.stringify(loonvrag), }); if (!res.ok) gooi nuwe Fout ("Kon nie indien nie"); gee res.json(); }, });

const { register, control, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(formSchema), defaultValues: { prys: 0, hoeveelheid: 1, belastingtarief: 0.1, tevredenheid: 3, hasAccount: "Nee", }, }); const prys = useWatch ({ beheer, naam: "prys" }); const quantity = useWatch({ beheer, naam: "hoeveelheid" }); const taxRate = useWatch({ beheer, naam: "belastingtarief" }); const hasAccount = useWatch({ beheer, naam: "hasAccount" }); const satisfaction = useWatch({ beheer, naam: "satisfaction" }); const subtotaal = useMemo(() => (prys ?? 0) * (hoeveelheid ?? 1), [prys, hoeveelheid]); const tax = useMemo(() => subtotaal * (belastingkoers ?? 0), [subtotaal, belastingkoers]); const totaal = useMemo(() => subtotaal + belasting, [subtotaal, belasting]); const onSubmit = (data: FormData) => mutation.mutate({ ...data, subtotaal, belasting, totaal }); const showSubmit = (stap === 2 && totaal < 100) || (stap === 3 && totaal >= 100)

gee terug (

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

{stap === 1 && ( <> 10%

Subtotaal: {subtotal}
Belasting: {tax}
Totaal: {total}
)}

{stap === 2 && ( <>

{hasAccount === "Ja" && ( <> )}

{tevredenheid >= 4 && (