Гэты артыкул спансуецца SurveyJS Існуе ментальная мадэль, якую падзяляюць большасць распрацоўшчыкаў React, нават не абмяркоўваючы яе ўслых. Што формы заўсёды павінны быць кампанентамі. Гэта азначае стэк, як:

React Hook Form для лакальнага стану (мінімальныя паўторныя візуалізацыі, эрганамічная рэгістрацыя поля, імператыўнае ўзаемадзеянне). Zod для праверкі (правільнасць уводу, праверка межаў, тыпабяспечны аналіз). React Query для бэкэнда: адпраўка, паўторныя спробы, кэшаванне, сінхранізацыя сервера і гэтак далей.

І для пераважнай большасці формаў — экранаў ўваходу, старонак налад, мадалаў CRUD — гэта працуе вельмі добра. Кожная частка выконвае сваю працу, яны складаюцца чыста, і вы можаце перайсці да частак вашага прыкладання, якія сапраўды адрозніваюць ваш прадукт. Але час ад часу ў форме пачынаюць назапашвацца такія рэчы, як правілы бачнасці, якія залежаць ад ранейшых адказаў, або вытворныя значэнні, якія каскадна перамяшчаюцца па трох палях. Магчыма, нават цэлыя старонкі, якія трэба прапускаць або паказваць на аснове агульнай сумы. Вы апрацоўваеце першую ўмоўную форму з дапамогай useWatch і ўбудаванай галіны, што нармальна. Потым яшчэ. Тады вы цягнецеся да superRefine для кадавання крос-полевых правілаў, якія ваша схема Zod не можа выказаць звычайным спосабам. Затым крокавая навігацыя пачынае прапускаць бізнес-логіку. У нейкі момант вы глядзіце на тое, што пабудавалі, і разумееце, што форма больш не з'яўляецца карыстальніцкім інтэрфейсам. Гэта хутчэй працэс прыняцця рашэнняў, і дрэва кампанентаў знаходзіцца менавіта там, дзе вы яго захавалі. Тут, я думаю, разумовая мадэль для формаў у React ламаецца, і ў гэтым сапраўды ніхто не вінаваты. Стэк RHF + Zod выдатны ў тым, для чаго ён быў распрацаваны. Праблема ў тым, што мы схільныя працягваць выкарыстоўваць яго пасля моманту, калі яго абстракцыі адпавядаюць праблеме, таму што альтэрнатыва патрабуе цалкам іншага мыслення аб формах. Гэты артыкул пра гэтую альтэрнатыву. Каб паказаць гэта, мы двойчы створым аднолькавую шматэтапную форму:

З React Hook Form + Zod падключаны да React Query для адпраўкі, З SurveyJS, які разглядае форму як даныя — простую схему JSON — а не як дрэва кампанентаў.

Тыя ж патрабаванні, тая ж умоўная логіка, той жа выклік API у канцы. Потым мы дакладна намалюем, што перамясцілася, а што засталося, і выкладзем практычны спосаб вырашыць, якую мадэль вы павінны выкарыстоўваць і калі. Форма, якую мы будуем:

Гэтая форма будзе выкарыстоўваць 4-этапны працэс: Крок 1: Дэталі

Імя (абавязкова), Электронная пошта (абавязкова, правільны фармат).

Крок 2: Заказ

Кошт за адзінку, колькасць, стаўка падатку, Вытворны: Прамежкавы вынік, падатак, Усяго.

Крок 3: Уліковы запіс і зваротная сувязь

У вас ёсць рахунак? (Так/Не) Калі Так → імя карыстальніка + пароль, патрабуецца абодва. Калі не → электронная пошта ўжо сабрана на этапе 1.

Ацэнка задаволенасці (1–5) Калі ≥ 4 → спытайце «Што вам спадабалася?» Калі ≤ 2 → спытайце «Што мы можам палепшыць?»

Крок 4: агляд

З'яўляецца, толькі калі агульная сума >= 100 Канчатковае прадстаўленне.

Гэта не крайнасць. Але гэтага дастаткова, каб выявіць архітэктурныя адрозненні. Частка 1: Кіраваны кампанентамі (React Hook Form + Zod) Ўстаноўка npm усталяваць рэакцыю-хук-форму zod @hookform/resolvers @tanstack/react-query

Схема Зода Пачнем са схемы Зод, таму што звычайна менавіта там усталёўваецца форма формы. На першых двух этапах — асабістыя дадзеныя і парадак уводу — усё проста: неабходныя радкі, лікі з мінімумам і пералік. Цікавая частка пачынаецца, калі вы спрабуеце выказаць умоўныя правілы.

імпарт { z } з "zod";

export const formSchema = z.object({ firstName: z.string().min(1, "Required"), email: z.string().email("Несапраўдны email"), price: z.number().min(0), quantity: z.number().min(1), taxRate: z.number(), hasAccount: z.enum(["Yes", "No"]), username: z.string().optional(), пароль: z.string().optional(), задаволенасць: z.number().min(1).max(5), positiveFeedback: z.string().optional(), паляпшэннеFeedback: z.string().optional(),}).superRefine((data, ctx) => { if (data.hasAccount === "Yes") { if (!data.username) { ctx.addIssue({ code: "custom", path: ["username"], message: "Required" });

if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ code: "custom", path: ["positiveFeedback"], message: "Калі ласка, падзяліцеся тым, што вам спадабалася" }); }

if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ код: "карыстальніцкі", шлях:["improvementFeedback"], паведамленне: "Калі ласка, скажыце нам, што палепшыць" }); }});

тып экспарту FormData = z.infer;

Звярніце ўвагу, што імя карыстальніка і пароль уводзяцца як optional(), нават калі яны ўмоўна абавязковыя, таму што схема ўзроўню тыпу Zod апісвае форму аб'екта, а не правілы, якія вызначаюць, калі палі маюць значэнне. Умоўнае патрабаванне павінна знаходзіцца ўнутры superRefine, які запускаецца пасля праверкі формы і мае доступ да поўнага аб'екта. Гэта расстанне не з'яўляецца недахопам; гэта як раз тое, для чаго прызначаны інструмент: superRefine - гэта тое, куды ідзе логіка перакрыжаванага поля, калі яна не можа быць выказана ў самой структуры схемы. Тут таксама характэрна тое, што гэтая схема не выражае. У ім няма канцэпцыі старонак, ні канцэпцыі таго, якія палі ў якім месцы бачныя, ні канцэпцыі навігацыі. Усё гэта будзе жыць у іншым месцы. Кампанент формы

імпарт { useForm, useWatch } з "react-hook-form"; імпарт { zodResolver } з "@hookform/resolvers/zod"; імпарт { useMutation } з "@tanstack/react-query"; імпарт { useState, useMemo } з "react"; імпарт { formSchema, увядзіце FormData } з "./schema";

const STEPS = ["дэталі", "заказ", "рахунак", "агляд"];

тып OrderPayload = FormData & {прамежкавы вынік: колькасць; падатак: колькасць; усяго: колькасць};

функцыя экспарту RHFMultiStepForm() {const [крок, setStep] = useState(0);

мутацыя const = useMutation({ mutationFn: async (карысная нагрузка: OrderPayload) => { const res = await fetch("/api/orders", { метад: "POST", загалоўкі: { "Content-Type": "application/json" }, цела: JSON.stringify(карысная нагрузка), }); if (!res.ok) throw new Error("Failed to submit"); вяртанне res.json(); }, });

const { register, control, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(formSchema), defaultValues: { price: 0, quantity: 1, taxRate: 0.1, satisfaction: 3, hasAccount: "Не", }, }); const price = useWatch({control, name: "price"}); const quantity = useWatch({ элемент кіравання, імя: "колькасць" }); const taxRate = useWatch({ элемент кіравання, імя: "taxRate" }); const hasAccount = useWatch({ элемент кіравання, імя: "hasAccount" }); const satisfaction = useWatch({control, name: "satisfaction"}); const subtotal = useMemo(() => (цана ?? 0) * (колькасць ?? 1), [цана, колькасць]); const tax = useMemo(() => прамежкавы вынік * (taxRate ?? 0), [прамежкавы вынік, taxRate]); const total = useMemo(() => прамежкавы вынік + падатак, [прамежкавы вынік, падатак]); const onSubmit = (data: FormData) => mutation.mutate({ ...data, subtotal, tax, total }); const showSubmit = (крок === 2 && усяго < 100) || (крок === 3 && усяго >= 100)

return (

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

{крок === 1 && ( <> <выбраць {...register("taxRate", { valueAsNumber: true })}> <параметр value="0.05">5%

Прамежкавы вынік: {subtotal}
Падатак: {tax}
Усяго: {total}
)}

{крок === 2 && ( <>

{hasAccount === "Так" && ( <> )}

{задаволенасць >= 4 && (