Šo rakstu sponsorē SurveyJS Pastāv prāta modelis, ar kuru vairums React izstrādātāju dalās, to nekad skaļi neapspriežot. Formām vienmēr ir jābūt sastāvdaļām. Tas nozīmē tādu kaudzi kā:

React Hook Form vietējai valstij (minimāla atkārtota renderēšana, ergonomiska lauka reģistrācija, obligāta mijiedarbība). Zod validācijai (ievades pareizība, robežu validācija, tipa droša parsēšana). Reaģēt uz aizmugursistēmas vaicājumu: iesniegšana, atkārtojumi, saglabāšana kešatmiņā, servera sinhronizācija un tā tālāk.

Un lielākajai daļai formu — jūsu pieteikšanās ekrānos, iestatījumu lapās, CRUD modāļos — tas darbojas ļoti labi. Katrs gabals veic savu darbu, tie ir tīri komponēti, un jūs varat pāriet uz lietojumprogrammas daļām, kas faktiski atšķir jūsu produktu. Taču ik pa laikam veidlapa sāk uzkrāt tādas lietas kā redzamības kārtulas, kas ir atkarīgas no iepriekšējām atbildēm, vai atvasinātās vērtības, kas tiek kaskādes cauri trim laukiem. Varbūt pat veselas lapas, kuras vajadzētu izlaist vai parādīt, pamatojoties uz kopējo kopējo skaitu. Jūs apstrādājat pirmo nosacījumu, izmantojot useWatch un iekļauto atzaru, un tas ir labi. Tad vēl viens. Pēc tam jūs izmantojat superRefine, lai kodētu starplauku noteikumus, kurus jūsu Zod shēma nevar izteikt parastajā veidā. Pēc tam soļu navigācija sāk nopludināt biznesa loģiku. Kādā brīdī jūs skatāties uz to, ko esat izveidojis, un saprotat, ka veidlapa vairs nav lietotāja saskarne. Tas ir vairāk lēmumu pieņemšanas process, un komponentu koks ir tieši tajā vietā, kur jūs to glabājāt. Šeit es domāju, ka React formu mentālais modelis sabojājas, un tā patiesībā nav neviena vaina. RHF + Zod steks ir lielisks tam, kam tas bija paredzēts. Problēma ir tāda, ka mums ir tendence to izmantot pēc tam, kad tā abstrakcijas atbilst problēmai, jo alternatīva prasa pilnīgi citu domāšanas veidu par formām. Šis raksts ir par šo alternatīvu. Lai to parādītu, mēs divreiz izveidosim tieši tādu pašu daudzpakāpju formu:

Ar React Hook Form + Zod pievienošanu React Query iesniegšanai, Ar SurveyJS, kas veidlapu apstrādā kā datus — vienkāršu JSON shēmu —, nevis kā komponentu koku.

Tās pašas prasības, tā pati nosacījuma loģika, viens un tas pats API izsaukums beigās. Pēc tam mēs precīzi norādīsim, kas tika pārvietots un kas palika, un izstrādāsim praktisku veidu, kā izlemt, kuru modeli un kad izmantot. Veidlapa, ko veidojam:

Šajā veidlapā tiks izmantota 4 pakāpju plūsma: 1. darbība. Sīkāka informācija

Vārds (obligāts), E-pasts (obligāts, derīgs formāts).

2. darbība: pasūtiet

Vienības cena, Daudzums, Nodokļa likme, Atvasināts: Starpsumma, Nodoklis, Kopā.

3. darbība: konts un atsauksmes

Vai jums ir konts? (Jā/Nē) Ja Jā → lietotājvārds + parole, abi ir nepieciešami. Ja Nē → e-pasts jau ir savākts 1. darbībā.

Apmierinātības vērtējums (1–5) Ja ≥ 4 → jautājiet “Kas jums patika?” Ja ≤ 2 → jautājiet “Ko mēs varam uzlabot?”

4. darbība. Pārskatiet

Parādās tikai tad, ja kopā >= 100 Galīgā iesniegšana.

Tas nav ekstrēmi. Bet ar to pietiek, lai atklātu arhitektūras atšķirības. 1. daļa: ar komponentiem darbināms (reaģēšanas āķa forma + Zod) Uzstādīšana npm instalēt react-hook-form zod @hookform/resolvers @tanstack/react-query

Zod shēma Sāksim ar Zod shēmu, jo tur parasti tiek noteikta formas forma. Pirmajās divās darbībās — personas informācija un pasūtījuma ievade — viss ir vienkārši: obligātās virknes, cipari ar minimumu un enum. Interesantā daļa sākas, mēģinot izteikt nosacījumus.

importēt { z } no "zod";

export const formSchema = z.object({ vārds: z.string().min(1, "Obligāts"), e-pasts: z.string().email("Nederīgs e-pasts"), cena: z.number().min(0), daudzums: z.number().min(1), taxRate: z.number(), hasAccount: ["]enum", "z.esum". z.string().optional(), parole: z.string().optional(), apmierinātība: z.skaitlis().min(1).maks.(5), pozitīvaAtsauksmes: z.string().optional(), uzlabojumsAtsauksmes: z.string().optional(),}).superRefine((data, ctxdata) =.=.ha { ifA (ctx) =. (!data.lietotājs } }

if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ kods: "pielāgots", ceļš: ["pozitīvāAtsauksmes"], ziņojums: "Lūdzu, dalieties ar to, kas jums patika" }); }

if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ kods: "pielāgots", ceļš:["improvementFeedback"], ziņojums: "Lūdzu, pastāstiet mums, ko uzlabot" }); }});

eksporta veids FormData = z.infer;

Ņemiet vērā, ka lietotājvārds un parole tiek ievadīti kā neobligāti (), lai gan tie ir nosacīti nepieciešami, jo Zod tipa līmeņa shēma apraksta objekta formu, nevis noteikumus, kas nosaka, kad lauki ir svarīgi. Nosacītajai prasībai ir jāatbilst superRefine, kas tiek izpildīta pēc formas apstiprināšanas un tai ir piekļuve visam objektam. Šī atdalīšana nav trūkums; tas ir tieši tas, kam rīks ir paredzēts: superRefine ir vieta, kur tiek izmantota starplauku loģika, kad to nevar izteikt pašā shēmas struktūrā. Šeit ir arī ievērojams tas, ko šī shēma neizsaka. Tajā nav jēdziena par lapām, nav jēdziena par to, kuri lauki ir redzami kādā vietā, un nav jēdziena par navigāciju. Tas viss dzīvos kaut kur citur. Veidlapas sastāvdaļa

importēt { useForm, useWatch } no "react-hook-form";importēt { zodResolver } no "@hookform/resolvers/zod";importēt { useMutation } no "@tanstack/react-query";importēt { useState, useMemo } no "react";importēt no: "formSchema";

const STEPS = ["detaļas", "pasūtījums", "konts", "pārskats"];

type OrderPayload = FormData & { starpsumma: numurs; nodoklis: numurs; kopā: skaits };

eksporta funkcija RHFMultiStepForm() { const [solis, setStep] = useState(0);

const mutācija = useMutation({ mutationFn: async (lietderīgā slodze: OrderPayload) => { const res = gaidīt fetch("/api/orders", { metode: "POST", galvenes: { "Content-Type": "application/json"}, pamatteksts: JSON.stringify(payload), }); if (!res.ok) throw new Error("Neizdevās iesniegt"); atgriezties res.json(); }, });

const { register, control, handleSubmit, formState: { errors }, } = useForm({ atrisinātājs: zodResolver(formSchema), defaultValues: { cena: 0, daudzums: 1, taxRate: 0.1, apmierinātība: 3, hasAccount: "Nē", }, }); const price = useWatch({ vadīkla, nosaukums: "cena" }); const daudzums = useWatch({ vadīkla, nosaukums: "daudzums"}); const taxRate = useWatch({ vadīkla, nosaukums: "taxRate"}); const hasAccount = useWatch({ vadīkla, nosaukums: "hasAccount" }); const satisfaction = useWatch({ vadīkla, nosaukums: "apmierinātība" }); const starpsumma = useMemo(() => (cena ?? 0) * (daudzums ?? 1), [cena, daudzums]); const tax = useMemo(() => starpsumma * (taxRate ?? 0), [starpsumma, taxRate]); const total = useMemo(() => starpsumma + nodoklis, [starpsumma, nodoklis]); const onSubmit = (dati: FormData) => mutation.mutate({ ...dati, starpsumma, nodoklis, kopsumma }); const showIesniegt = (solis === 2 && kopā < 100) || (solis === 3 && kopā >= 100)

return (

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

{solis === 1 && ( <>

Starpsumma: {subtotal}
Nodoklis: {tax}
Kopā: {total}
)}

{step === 2 && ( <>

{hasAccount === "Jā" && ( <> )}

{apmierinātība >= 4 && (