Ĉi tiu artikolo estas sponsorita de SurveyJS Estas mensa modelo, kiun plej multaj programistoj de React dividas sen iam diskuti ĝin laŭte. Ke formoj ĉiam supozeble estas komponantoj. Ĉi tio signifas stakon kiel:

React Hook Form por loka ŝtato (minimumaj re-bildoj, ergonomia kampo-registrado, imperativa interago). Zod por validumado (eniga korekteco, limvalidigo, tip-sekura analizo). Reagi Demandon por backend: submetado, reprovoj, kaŝmemoro, servila sinkronigo, ktp.

Kaj por la granda plimulto de formoj - viaj ensalut-ekranoj, viaj agordaj paĝoj, viaj CRUD-modaloj - tio funkcias vere bone. Ĉiu peco faras sian laboron, ili komponas pure, kaj vi povas pluiri al la partoj de via aplikaĵo, kiuj efektive diferencas vian produkton. Sed de tempo al tempo, formo komencas amasigi aferojn kiel videblecoj, kiuj dependas de pli fruaj respondoj, aŭ derivitaj valoroj, kiuj kaskadas tra tri kampoj. Eble eĉ tutaj paĝoj kiuj devus esti preterlasitaj aŭ montritaj surbaze de kuranta totalo. Vi pritraktas la unuan kondiĉon per useWatch kaj enlinia branĉo, kio estas bone. Poste alia. Tiam vi serĉas superRefine por kodi transkampajn regulojn, kiujn via Zod-skemo ne povas esprimi laŭ la normala maniero. Tiam, paŝa navigado komencas liki komercan logikon. Iam vi rigardas tion, kion vi konstruis kaj rimarkas, ke la formo ne plu estas vere UI. Ĝi estas pli de decida procezo, kaj la kompona arbo estas ĝuste kie vi hazarde konservis ĝin. Jen kie mi pensas, ke la mensa modelo por formoj en React rompiĝas, kaj ĝi estas vere neniu kulpo. La stako RHF + Zod estas bonega pri tio, por kio ĝi estis desegnita. La afero estas, ke ni emas daŭre uzi ĝin preter la punkto kie ĝiaj abstraktaĵoj kongruas kun la problemo ĉar la alternativo postulas tute malsaman manieron pensi pri formoj. Ĉi tiu artikolo temas pri tiu alternativo. Por montri ĉi tion, ni konstruos la ĝustan saman plurpaŝan formon dufoje:

Kun React Hook Form + Zod kablita al React Query por submetado, Kun SurveyJS, kiu traktas formon kiel datumojn - simplan JSON-skemon - prefere ol komponan arbon.

Samaj postuloj, sama kondiĉa logiko, sama API-voko ĉe la fino. Tiam ni mapos precize kio moviĝis kaj kio restis, kaj aranĝos praktikan manieron decidi kiun modelon vi devas uzi, kaj kiam. La formo, kiun ni konstruas:

Ĉi tiu formo uzos 4-paŝan fluon: Paŝo 1: Detaloj

Antaŭnomo (postulata), Retpoŝto (postulata, valida formato).

Paŝo 2: Ordonu

Unua prezo, Kvanto, Imposta indico, Derivita: Subtotalo, Imposto, Entute.

Paŝo 3: Konto kaj Reago

Ĉu vi havas konton? (Jes/Ne) Se Jes → uzantnomo + pasvorto, ambaŭ necesas. Se Ne → retpoŝto jam kolektita en paŝo 1.

Kontentiga takso (1–5) Se ≥ 4 → demandu "Kion vi ŝatis?" Se ≤ 2 → demandu "Kion ni povas plibonigi?"

Paŝo 4: Revizio

Nur aperas se entute >= 100 Fina submetiĝo.

Ĉi tio ne estas ekstrema. Sed sufiĉas elmontri arkitekturajn diferencojn. Parto 1: Komponanto-Movita (Reagi Hoko-Formo + Zod) Instalado npm instali react-hook-form zod @hookform/resolvers @tanstack/react-query

Zod-Skemo Ni komencu per la Zod-skemo, ĉar tie kutime stariĝas la formo de la formo. Por la unuaj du paŝoj — personaj detaloj kaj mendaj enigaĵoj — ĉio estas simpla: postulataj ĉenoj, nombroj kun minimumoj kaj enumo. La interesa parto komenciĝas kiam oni provas esprimi la kondiĉajn regulojn.

importi { z } el "zod";

eksporto const formSchema = z.object({ firstName: z.string().min(1, "Bezonata"), retpoŝto: z.string().email("Nevalida retpoŝto"), prezo: z.number().min(0), kvanto: z.number().min(1), taxRate: z.number(), hasKonto: z.number(), has Account (No["]), uzantnomo (No["]), "z.esum" z.string().optional(), pasvorto: z.string().optional(), kontentigo: z.number().min(1).max(5), pozitivaResago: z.string().optional(), plibonigoResago: z.string().optional(),}).superRefine((datumoj, ctx) => { if (datenoj, ctx) => { if (datenoj, ctx) => { if (datenoj, ctx) => { if (datumoj) === "Yes Account. ctx.addIssue({ kodo: "persona", vojo: ["uzantnomo"], mesaĝo: "Bezonata" }); } if (!data.password || data.password.length < 6) { ctx.addIssue({ kodo: "persona", vojo: ["pasvorto"], mesaĝo: "Min 6 signoj}"});

if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ kodo: "persona", vojo: ["positiveFeedback"], mesaĝo: "Bonvolu dividi kion vi ŝatis" }); }

if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue ({ kodo: "persona", vojo:["improvementFeedback"], message: "Bonvolu diri al ni kion plibonigi"}); }});

eksporta tipo FormData = z.infer;

Rimarku, ke uzantnomo kaj pasvorto estas tajpitaj kiel laŭvolaj () kvankam ili estas kondiĉe postulataj ĉar la tipnivela skemo de Zod priskribas la formon de la objekto, ne la regulojn regantajn kiam kampoj gravas. La kondiĉa postulo devas vivi ene de superRefine, kiu funkcias post kiam la formo estas validigita kaj havas aliron al la plena objekto. Tiu disiĝo ne estas manko; ĝi estas nur por kio la ilo estas dizajnita: superRefine estas kie transkampa logiko iras kiam ĝi ne povas esti esprimita en la skemstrukturo mem. Kio estas ankaŭ rimarkinda ĉi tie estas tio, kion ĉi tiu skemo ne esprimas. Ĝi havas neniun koncepton de paĝoj, neniu koncepto de kiuj kampoj estas videblaj ĉe kiu punkto, kaj neniu koncepto de navigado. Ĉio tio loĝos aliloke. Forma Komponanto

importu { useForm, useWatch } el "react-hook-form";importu { zodResolver } el "@hookform/resolvers/zod";importu { useMutation } el "@tanstack/react-query";importu { useState, useMemo } el "react";import { formSchema, tajpu FormDataschema};

const STEPS = ["detaloj", "mendo", "konto", "recenzo"];

type OrderPayload = FormData & { subtotalo: nombro; imposto: nombro; totalo: nombro };

eksportfunkcio RHFMultiStepForm () { const [step, setStep] = uzoStato (0);

konst mutacio = uzuMutacion({ mutationFn: nesinkronigita (utila ŝarĝo: OrderPayload) => { const res = atendi preni ("/api/mendoj", { metodo: "POST", kaplinioj: { "Content-Type": "application/json" }, korpo: JSON.stringify(utila ŝarĝo), }); if (!res.ok) throw new Eraro ("Malsukcesis sendi"); resendi res.json(); }, });

const { registri, kontrolo, manipuliSubmit, formState: { eraroj }, } = useForm({ solvanto: zodResolver(formSchema), defaultValues: { prezo: 0, kvanto: 1, impostoRate: 0.1, kontento: 3, havasKonton: "Ne", }, }); const prezo = uzu Watch({ kontrolo, nomo: "prezo" }); konst kvanto = uzu Watch({ kontrolo, nomo: "kvanto" }); const impostoKuzo = uzu Rigardi({ kontrolo, nomo: "imposta indico" }); const havasKonton = useWatch({ kontrolo, nomo: "hasKonton" }); konst kontento = uzu Watch({ kontrolo, nomo: "kontento" }); const subtotal = uzuMemo(() => (prezo ?? 0) * (kvanto ?? 1), [prezo, kvanto]); const imposto = uzuMemo(() => subtotalo * (impostKiro ?? 0), [subtotalo, impostoRate]); const total = useMemo(() => subtotalo + imposto, [subtotalo, imposto]); const onSubmit = (datenoj: FormData) => mutacio.mutate({ ...datumoj, subtotalo, imposto, totalo }); const showSubmit = (paŝo === 2 && totalo < 100) || (paŝo === 3 && totalo >= 100)

return (

{paŝo === 0 && ( <> )}

{paŝo === 1 && ( <> : 5%

Subtotalo: {subtotal}
Imposto: {tax}
Sumo: {total}
)}

{paŝo === 2 && ( <>

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

{kontento >= 4 && ( )}

{kontento <= 2 && (