Бул макала SurveyJS тарабынан каржыланган Көпчүлүк React иштеп чыгуучулары аны эч качан катуу талкуулабастан бөлүшө турган психикалык модель бар. Бул формалар ар дайым компоненттер болушу керек. Бул стек дегенди билдирет:

Жергиликтүү штат үчүн React Hook формасы (минималдуу кайра көрсөтүү, эргономикалык талааны каттоо, императивдик өз ара аракеттенүү). Валидация үчүн зод (киргизүүнүн тууралыгы, чек араны текшерүү, типтин коопсуз талдоосу). Backend үчүн React Query: тапшыруу, кайра аракет кылуу, кэштөө, серверди синхрондоштуруу ж.б.у.с.

Ал эми формалардын басымдуу көпчүлүгү үчүн – сиздин кирүү экрандарыңыз, жөндөө беттериңиз, CRUD модалдарыңыз – бул абдан жакшы иштейт. Ар бир бөлүгү өз милдетин аткарат, алар таза түзүшөт жана сиз колдонмоңуздун продуктуну айырмалай турган бөлүктөрүнө өтө аласыз. Бирок маал-маалы менен форма мурунку жоопторго көз каранды көрүнүү эрежелери же үч талаа аркылуу каскаддан өткөн алынган баалуулуктар сыяктуу нерселерди топтой баштайт. Мүмкүн, атүгүл бүтүндөй барактарды өткөрүп жиберүү же иштеп жаткан сумманын негизинде көрсөтүү керек. Биринчи шартты useWatch жана сап бутагы менен иштетесиз, бул жакшы. Андан кийин башка. Андан кийин Zod схемаңыз кадимки жол менен билдире албаган кайчылаш талаа эрежелерин коддоо үчүн superRefineге жетип жатасыз. Андан кийин, кадам багыттоо бизнес логикасын агып баштайт. Кайсы бир учурда, сиз курган нерсеңизди карап, форма мындан ары UI эмес экенин түшүнөсүз. Бул чечим кабыл алуу процесси жана компонент дарагы сиз аны сактаган жерде. Бул жерде менимче, React'теги формалардын психикалык модели бузулат жана бул чындыгында эч кимдин күнөөсү эмес. RHF + Zod стек ал үчүн иштелип чыккан мыкты. Маселе, биз аны анын абстракциялары көйгөйгө дал келген чекитке чейин колдоно беребиз, анткени альтернатива формалар жөнүндө башкача ой жүгүртүүнү талап кылат. Бул макала ошол альтернатива жөнүндө. Муну көрсөтүү үчүн, биз бир эле көп баскычтуу форманы эки жолу курабыз:

React Hook Form + Zod менен React Query тапшыруу үчүн, Форманы компонент катары эмес, жөнөкөй JSON схемасы катары караган SurveyJS менен.

Ошол эле талаптар, ошол эле шарттуу логика, аягында ошол эле API чалуу. Андан кийин биз эмне көчүп, эмне калганын так картага түшүрөбүз жана кайсы моделди жана качан колдонуу керектигин чечүүнүн практикалык жолун сунуштайбыз. Биз түзүп жаткан форма:

Бул форма 4 кадамдуу агымды колдонот: 1-кадам: чоо-жайы

Аты-жөнү (талап кылынат), Электрондук почта (талап кылынат, жарактуу формат).

2-кадам: Заказ

бирдик баасы, Саны, Салык ставкасы, Алынган: Аралык сумма, салык, Бардыгы.

3-кадам: Каттоо эсеби жана пикир

Сиздин аккаунтуңуз барбы? (Ооба/Жок) Ооба → колдонуучу аты + сырсөз болсо, экөө тең талап кылынат. Эгерде Жок → электрондук почта 1-кадамда чогултулган.

Канааттануу рейтинги (1–5) Эгерде ≥ 4 → "Сизге эмне жакты?" деп сураңыз. Эгерде ≤ 2 → "Эмнени жакшырта алабыз?" деп сураңыз.

4-кадам: карап чыгуу

Жалпы >= 100 болсо гана көрүнөт Акыркы тапшыруу.

Бул экстремалдуу эмес. Бирок бул архитектуралык айырмачылыктарды ачуу үчүн жетиштүү. 1-бөлүк: Компонентке негизделген (React Hook Form + Zod) Орнотуу npm орнотуу react-hook-form zod @hookform/resolvers @tanstack/react-query

Zod схемасы Зод схемасынан баштайлы, анткени форманын формасы көбүнчө ошол жерде орнойт. Алгачкы эки кадам үчүн - жеке маалыматтар жана буйрутма киргизүү - баары жөнөкөй: талап кылынган саптар, минималдуу сандар жана энум. Кызыктуу бөлүгү шарттуу эрежелерди айтууга аракет кылганда башталат.

"zod"дан { z } импорттоо;

export const formSchema = z.object({ firstName: z.string().min(1, "Талап кылынган"), email: z.string().email("Жарамсыз электрондук почта"), баасы: z.number().min(0), саны: z.number().min(1), taxRate: z.number(), hasAccount(), username(]), "Yok":" z.string().optional(), сырсөз: z.string().optional(), канааттануу: z.number().min(1).max(5), оң пикир: z.string().optional(), improvementFeedback: z.string().optional(),}).superRefine((дата, ctxda) ="s. (!data.username) { ctx.addIssue({код: "колдонуучунун аты", жол: ["колдонуучунун аты"], билдирүү: "Талап кылынат" }); }

if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({код: "адат", жол: ["positiveFeedback"], билдирүү: "Сизге жаккан нерсени бөлүшүңүз" }); }

if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({код: "ыңгайлаштырылган", жол:["improvementFeedback"], билдирүү: "Бизге эмнени жакшыртуу керектигин айтыңыз"}); }});

экспорт түрү FormData = z.infer;

Колдонуучунун аты жана сырсөз шарттуу түрдө талап кылынса да, кошумча() катары терилгенине көңүл буруңуз, анткени Zodдун тип деңгээлиндеги схемасы талаалар маанилүү болгон учурда башкарган эрежелерди эмес, объекттин формасын сүрөттөйт. Шарттуу талап форма текшерилгенден кийин иштей турган жана толук объектке кирүү мүмкүнчүлүгү бар superRefine ичинде жашашы керек. Бул бөлүнүү кемчилик эмес; бул жөн гана курал эмне үчүн иштелип чыккан: superRefine - бул схема түзүмүндө чагылдырылбаганда кайчылаш талаа логикасы кайда барат. Бул жерде дагы бир маанилүү нерсе, бул схема эмнени билдирбейт. Анын беттер түшүнүгү жок, кайсы талаалар кайсы жерде көрүнөрү жана навигация түшүнүгү жок. Мунун баары башка жерде жашайт. Форма компоненти

"react-hook-form"дан { useForm, useWatch } импорттоо; "@hookform/resolvers/zod"дан { zodResolver } импорттоо; "@tanstack/react-query"ден { useMutation } импорттоо; "реакциядан" { useState, useMemo } импорттоо; Import { formSchema, "."

const STEPS = ["деталдар", "заказ", "эсеп", "карап чыгуу"];

түрү OrderPayload = FormData & {аралык: саны; салык: саны; жалпы: саны };

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

const мутация = useMutation({ mutationFn: асинхрондук (пайдалуу жүк: OrderPayload) => { const res = күтүү алып келүү("/api/orders", { ыкма: "POST", аталыштар: { "Content-Type": "application/json" }, дене: JSON.stringify(пайдалуу жүк), }); if (!res.ok) throw new Error("Failed to submit"); return res.json(); }, });

const { регистр, контроль, handleSubmit, formState: { каталар }, } = useForm({чечилүүчү: zodResolver(formSchema), defaultValues: {баасы: 0, саны: 1, taxRate: 0,1, канааттандыруу: 3, hasAccount: "Жок", },}); const баасы = useWatch({контроль, аты: "баа"}); const quantity = useWatch({контроль, аты: "саны"}); const taxRate = useWatch({контрол, аты: "taxRate"}); const hasAccount = useWatch({контрол, аты: "hasAccount"}); const канааттануу = useWatch({контроль, аты: "канааттануу"}); const subtotal = useMemo(() => (баасы ?? 0) * (саны ?? 1), [баасы, саны]); const салык = useMemo(() => орто сумма * (taxRate ?? 0), [жалпы сумма, taxRate]); const total = useMemo(() => орто сумма + салык, [жалпы сумма, салык]); const onSubmit = (маалыматтар: FormData) => mutation.mutate({ ...маалыматтар, орто сумма, салык, жалпы }); const showSubmit = (кадам === 2 && жалпы < 100) || (кадам === 3 && жалпы >= 100)

return (

{кадам === 0 && ( <> )}

{кадам === 1 && ( <>

Аралык жалпы: {subtotal}
Салык: {салык}
Бардыгы: {бардыгы}
)}

{кадам === 2 && ( <>

{hasAccount === "Ооба" && ( <> <киргизүү {...register("username")} placeholder="Колдонуучунун аты" /> <киргизүү {...register("password")} placeholder="Сырсөз" /> )}

<киргизүү түрү = "сан" {...каттоо("канааттануу", { valueAsNumber: чыныгы })} />

{канааттануу >= 4 && (