Ta članek sponzorira SurveyJS Obstaja miselni model, ki ga deli večina React razvijalcev, ne da bi o njem kdaj razpravljali na glas. Da naj bi bili obrazci vedno sestavni deli. To pomeni sklad, kot je:

Obrazec React Hook za lokalno stanje (minimalne ponovne upodobitve, ergonomska registracija polja, nujna interakcija). Zod za preverjanje veljavnosti (pravilnost vnosa, preverjanje meje, varno razčlenjevanje). Poizvedba React za zaledje: predložitev, ponovni poskusi, predpomnjenje, sinhronizacija strežnika itd.

In za veliko večino obrazcev – vaše prijavne zaslone, vaše nastavitvene strani, vaše CRUD modale – to deluje zelo dobro. Vsak kos opravlja svoje delo, sestavijo se čisto in lahko nadaljujete z deli vaše aplikacije, ki dejansko razlikujejo vaš izdelek. Toda obrazec vsake toliko časa začne kopičiti stvari, kot so pravila vidnosti, ki so odvisna od prejšnjih odgovorov, ali izpeljane vrednosti, ki se vrstijo skozi tri polja. Morda celo celotne strani, ki bi jih bilo treba preskočiti ali prikazati glede na tekočo vsoto. Prvi pogojnik obravnavate z useWatch in inline vejo, kar je v redu. Potem še eno. Potem posežete po superRefine za kodiranje pravil med polji, ki jih vaša shema Zod ne more izraziti na običajen način. Nato navigacija po korakih začne uhajati iz poslovne logike. Na neki točki pogledate, kaj ste zgradili, in ugotovite, da obrazec v resnici ni več uporabniški vmesnik. To je bolj postopek odločanja in drevo komponent je točno tam, kjer ste ga shranili. Tukaj mislim, da se miselni model za obrazce v Reactu pokvari in za to res ni nihče kriv. Sklad RHF + Zod je odličen v tem, za kar je bil zasnovan. Težava je v tem, da jo še naprej uporabljamo čez točko, ko se njene abstrakcije ujemajo s problemom, ker alternativa zahteva povsem drugačen način razmišljanja o oblikah. Ta članek govori o tej alternativi. Da bi to pokazali, bomo dvakrat zgradili popolnoma enak večstopenjski obrazec:

Z React Hook Form + Zod je povezan z React Query za predložitev, S SurveyJS, ki obrazec obravnava kot podatke – preprosto shemo JSON – in ne kot drevo komponent.

Iste zahteve, ista pogojna logika, isti klic API-ja na koncu. Nato bomo natanko preslikali, kaj se je premaknilo in kaj ostalo, ter predstavili praktičen način za odločitev, kateri model bi morali uporabiti in kdaj. Obrazec, ki ga gradimo:

Ta obrazec bo uporabljal potek v 4 korakih: 1. korak: podrobnosti

Ime (obvezno), E-pošta (obvezno, veljavna oblika).

2. korak: Naročilo

cena na enoto, količina, davčna stopnja, Izpeljano: Vmesni seštevek, davek, Skupaj.

3. korak: račun in povratne informacije

Ali imate račun? (da/ne) Če je odgovor Da → uporabniško ime + geslo, potrebno je oboje. Če ne → e-pošta je že zbrana v 1. koraku.

Ocena zadovoljstva (1–5) Če je ≥ 4 → vprašajte "Kaj vam je bilo všeč?" Če je ≤ 2 → vprašajte "Kaj lahko izboljšamo?"

4. korak: Pregled

Prikaže se le, če je skupno >= 100 Končna oddaja.

To ni ekstrem. Toda dovolj je, da izpostavimo arhitekturne razlike. 1. del: na podlagi komponent (React Hook Form + Zod) Namestitev npm namestite obliko react-hook zod @hookform/resolvers @tanstack/react-query

Zod shema Začnimo s shemo Zod, ker se tam običajno vzpostavi oblika obrazca. Za prva dva koraka – osebne podatke in vnose naročil – je vse preprosto: zahtevani nizi, števila z minimumi in enum. Zanimiv del se začne, ko poskušate izraziti pogojna pravila.

uvoz { 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(), geslo: z.string().optional(), zadovoljstvo: z.number().min(1).max(5), positiveFeedback: z.string().optional(), izboljšanjeFeedback: z.string().optional(),}).superRefine((data, ctx) => { if (data.hasAccount === "Yes") { if (!data.username) { ctx.addIssue({ code: "custom", path: ["username"], message: "Required" } } if (!data.password || data.password.length < 6) { ctx.addIssue({ code: "custom", path: ["password"], message: "Min 6 characters" });

if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ code: "custom", path: ["positiveFeedback"], message: "Prosimo, delite, kar vam je bilo všeč" }); }

if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ koda: "po meri", pot:["improvementFeedback"], sporočilo: "Prosimo, povejte nam, kaj naj izboljšamo" }); }});

izvozni tip FormData = z.infer;

Upoštevajte, da sta uporabniško ime in geslo vnesena kot optional(), čeprav sta pogojno obvezna, ker Zodova shema na ravni tipa opisuje obliko predmeta in ne pravil, ki urejajo, kdaj so polja pomembna. Pogojna zahteva mora živeti znotraj superRefine, ki se zažene po potrditvi oblike in ima dostop do celotnega objekta. Ta ločitev ni napaka; to je tisto, za kar je orodje zasnovano: superRefine je tisto, kamor gre logika navzkrižnih polj, ko je ni mogoče izraziti v sami strukturi sheme. Tukaj je opazno tudi to, česar ta shema ne izraža. Nima koncepta strani, pojma o tem, katera polja so vidna na kateri točki, in koncepta navigacije. Vse to bo živelo nekje drugje. Komponenta obrazca

uvoz { useForm, useWatch } iz "react-hook-form"; uvoz { zodResolver } iz "@hookform/resolvers/zod"; uvoz { useMutation } iz "@tanstack/react-query"; uvoz { useState, useMemo } iz "react"; uvoz { formSchema, tip FormData } iz "./schema";

const STEPS = ["podrobnosti", "naročilo", "račun", "pregled"];

type OrderPayload = FormData & { subtotal: number; davek: številka; skupaj: število };

izvozna funkcija RHFMultiStepForm() { const [korak, setStep] = useState(0);

mutacija const = useMutation({ mutationFn: async (payload: OrderPayload) => { const res = await fetch("/api/orders", { metoda: "POST", headers: { "Content-Type": "application/json" }, telo: JSON.stringify(payload), }); if (!res.ok) throw new Error("Fail to submit"); vrni res.json(); }, });

const { register, control, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(formSchema), defaultValues: { price: 0, quantity: 1, taxRate: 0.1, satisfaction: 3, hasAccount: "Ne", }, }); const price = useWatch({ control, name: "price" }); const količina = useWatch({ nadzor, ime: "količina" }); const taxRate = useWatch({ control, name: "taxRate" }); const hasAccount = useWatch({ control, name: "hasAccount" }); const satisfaction = useWatch({ control, name: "satisfaction" }); const subtotal = useMemo(() => (cena ?? 0) * (količina ?? 1), [cena, količina]); const davek = useMemo(() => delni seštevek * (davčna stopnja ?? 0), [vmesni seštevek, davčna stopnja]); const total = useMemo(() => vmesni seštevek + davek, [vmesni seštevek, davek]); const onSubmit = (data: FormData) => mutation.mutate({ ...data, subtotal, tax, total }); const showSubmit = (korak === 2 && skupno < 100) || (korak === 3 && skupno >= 100)

return (

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

{korak === 1 && ( <> 5 %

Vmesni seštevek: {subtotal}
Davek: {tax}
Skupaj: {total}
)}

{korak === 2 && ( <>

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

{zadovoljstvo >= 4 && (