Оваа статија е спонзорирана од SurveyJS Има ментален модел што повеќето програмери на React го споделуваат без воопшто да разговараат за тоа гласно. Дека формите секогаш треба да бидат компоненти. Ова значи оџак како:

Формулар React Hook за локална состојба (минимално пререндерирање, ергономска регистрација на теренот, императивна интеракција). Зод за валидација (коректност на внесувањето, валидација на границите, парсирање безбедно за тип). Реагирајте го барањето за задниот дел: поднесување, повторни обиди, кеширање, синхронизација на серверот и така натаму.

И за огромното мнозинство форми - вашите екрани за најавување, вашите страници за поставки, вашите модали 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 инсталирај react-hook-form zod @hookform/resolvers @tanstack/react-query

Зод шема Да почнеме со Зод шемата, бидејќи обично таму се утврдува обликот на формата. За првите два чекори - лични податоци и внесување нарачки - сè е едноставно: потребни низи, броеви со минимум и број. Интересниот дел започнува кога се обидувате да ги изразите условните правила.

увоз { z } од „zod“;

извоз const formSchema = z.object({ firstName: z.string().min(1, "Задолжително"), email: z.string().email("Неважечка е-пошта"), цена: z.number().min(0), количина: z.number().min(1), даночна стапка: z.number(), has Account:(No"]Yes. z.string().optional(), лозинка: z.string().optional(), задоволство: z.number().min(1).max(5), positiveFeedback: z.string().optional(), подобрувањаПовратни информации: z.string().optional(),}).superRefine((податоци, {ifsda) =Yfcta"=>>> (!data.username) { ctx.addIssue({ код: "прилагодено", патека: ["корисничко име"], порака: "Задолжително" } } if (!data.password || data.password.length < 6) {ctx.addIssue({ code: "custom", знакот"}); }

if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ код: „прилагодено“, патека: [„позитивни повратни информации“], порака: „Ве молиме споделете што ви се допаѓа“ }); }

if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ код: „прилагодено“, патека:["improvementFeedback"], порака: "Ве молиме кажете ни што да подобриме" }); }});

тип на извоз FormData = z.infer;

Забележете дека корисничкото име и лозинката се напишани како опционални() иако тие се условно потребни затоа што шемата на ниво на тип на Зод ја опишува формата на објектот, а не правилата кои регулираат кога полињата се важни. Условното барање мора да живее во суперРефине, кое работи откако формата е потврдена и има пристап до целосниот објект. Тоа разделување не е мана; Тоа е токму она за што е дизајнирана алатката: superRefine е местото каде што оди логиката меѓу полињата кога не може да се изрази во самата структура на шемата. Она што е исто така забележливо овде е она што оваа шема не го изразува. Нема концепт за страници, нема концепт за тоа кои полиња се видливи во која точка и нема концепт за навигација. Сето тоа ќе живее на друго место. Компонента на формуларот

увоз { useForm, useWatch } од "react-hook-form";увези { zodResolver } од "@hookform/resolvers/zod";увези { useMutation } од "@tanstack/react-query";увоз {useState, useMemo } од "react";увези тип {formSchema";

const ЧЕКОРИ = ["детали", "нарачка", "сметка", "преглед"];

тип OrderPayload = FormData & { subtotal: number; данок: број; вкупно: број };

функција за извоз RHFMultiStepForm() { const [чекор, setStep] = useState(0);

const мутација = useMutation({ mutationFn: асинхронизирано (оптоварување: OrderPayload) => { const res = чекај донеси("/api/orders", { метод: "POST", заглавија: { "Content-Type": "application/json" }, тело: JSON.stringify(payload), }); ако (!res.ok) фрли нова Грешка ("Не успеа да се достави"); врати рес.json(); }, });

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

врати (

{step === 0 && ( <> <внеси {...register("firstName")} placeholder="First Name" /> )}

{step === 1 && ( <>

Подвкупно: {subtotal}
Данок: {tax}
Вкупно: {total}
)}

{step === 2 && ( <>

{hasAccount === "Да" && ( <> <внеси {...register("username")} placeholder="Корисничко име" /> )}

{задоволство >= 4 && (