Гэты артыкул спансуецца 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
return (
);}Глядзіце Pen SurveyJS-03-RHF [раздвоены] ад sixthextinction. Тут адбываецца даволі шмат, і варта запаволіцца, каб заўважыць, чым усё скончылася.
Вытворныя значэнні — прамежкавы вынік, падатак, агульны — вылічваюцца ў кампаненце праз useWatch і useMemo, таму што яны залежаць ад жывых значэнняў палёў і для іх няма іншага натуральнага месца. Правілы бачнасці для імя карыстальніка, пароля, станоўчых водгукаў і паляпшэнняў водгукаў існуюць у JSX як убудаваныя ўмоўныя ўмовы. Логіка пропуску крокаў — старонка агляду з'яўляецца толькі тады, калі агульная колькасць >= 100 — убудавана ў зменную showSubmit і ўмовы візуалізацыі на кроку 3. Сама навігацыя - гэта толькі лічыльнік useState, які мы павялічваем уручную. React Query апрацоўвае паўторныя спробы, кэшаванне і ануляванне. Форма проста выклікае mutation.mutate з праверанымі дадзенымі.
Нічога з гэтага само па сабе не з'яўляецца памылковым. Гэта па-ранейшаму ідыёматычны React, і кампанент даволі эфектыўны дзякуючы таму, як RHF ізалюе рэрэндэры. Але калі б вы перадалі гэта камусьці, хто гэтага не пісаў, і папрасілі растлумачыць, пры якіх умовах з'яўляецца старонка рэцэнзіі, яны павінны былі б прасачыць праз showSubmit, умову візуалізацыі кроку 3 і логіку кнопкі навігацыі — тры розныя месцы — каб аднавіць правіла, якое магло быць выкладзена ў адным радку. Форма працуе, так, але паводзіны на самай справе не паддаюцца кантролю як сістэма. Яго трэба выканаць у думках. Што яшчэ больш важна, змяненне гэтага патрабуе ўдзелу інжынераў. Нават невялікая налада, напрыклад наладжванне моманту праверкі, азначае рэдагаванне кампанента, абнаўленне праверкі, адкрыццё запыту на выцягванне, чаканне разгляду і паўторнае разгортванне. Частка 2: На аснове схемы (SurveyJS) Зараз давайце пабудуем той жа паток з дапамогай схемы. Ўстаноўка npm усталяваць survey-core survey-react-ui @tanstack/react-query
survey-coreНезалежны ад платформы механізм выканання з ліцэнзіяй Масачусецкага тэхналагічнага інстытута, які забяспечвае рэндэрынг форм SurveyJS - тая частка, якая нас тут цікавіць. Ён бярэ схему JSON, будуе з яе ўнутраную мадэль і апрацоўвае ўсё, што ў адваротным выпадку было б у вашым кампаненце React: ацэнка выразаў бачнасці, вылічэнне вытворных значэнняў, кіраванне станам старонкі, адсочванне праверкі і прыняцце рашэнняў аб тым, што азначае «завершана», улічваючы, якія старонкі сапраўды былі паказаны.
survey-react-uiУзровень інтэрфейсу / візуалізацыі, які злучае гэтую мадэль з React. Па сутнасці, гэта кампанент
Разам яны даюць вам цалкам функцыянальную шматстаронкавую форму без напісання ніводнага радка патоку кіравання. Сам фармат схемы, як было сказана раней, проста JSON - без DSL або чагосьці ўласнага. Вы можаце ўбудаваць яго, імпартаваць з файла, атрымаць з API або захаваць у слупку базы дадзеных і ўвільгатняць падчас выканання. Тая ж форма, што і дадзеныя Вось тая ж форма, на гэты раз выяўленая як аб'ект JSON. Схема вызначае ўсё: структуру, праверку, правілы бачнасці, вытворныя вылічэнні, навігацыю па старонках — і перадае гэта мадэлі, якая ацэньвае гэта падчас выканання. Вось як гэта выглядае цалкам:
export const surveySchema = { title: "Order Flow", showProgressBar: "top", pages: [ { name: "details", elements: [ { type: "text", name: "firstName", isRequired: true }, { type: "text", name: "email", inputType: "email", isRequired: true, validators: [{ type: "email", text: "Invalid email" }] } ] }, { name: "order", elements: [ { type: "text", name: "price", inputType: "number", defaultValue: 0 }, { type: "text", name: "quantity", inputType: "number", defaultValue: 1 }, { type: "dropdown",name: "taxRate", defaultValue: 0.1, options: [ { value: 0.05, text: "5%" }, { value: 0.1, text: "10%" }, { value: 0.15, text: "15%" } ] }, { type: "expression", name: "subtotal", expression: "{price} {quantity}" }, { type: "expression", name: "tax", expression: "{subtotal} {taxRate}" }, { type: "expression", name: "total", expression: "{subtotal} + {tax}" } ] }, { name: "account", elements: [ { type: "radiogroup", name: "hasAccount", options: ["Yes", "No"] }, { type: "text", name: "імя карыстальніка", visibleIf: "{hasAccount} = 'Так'", isRequired: true }, { type: "text", name: "password", inputType: "password", visibleIf: "{hasAccount} = 'Yes'", isRequired: true, валідатары: [{ type: "text", minLength: 6, text: "Мінімум 6 сімвалаў" }] }, { type: "ацэнка", імя: "задаволенасць", хуткасцьМін: 1, максімальная адзнака: 5 }, { тып: "каментар", імя: "positiveFeedback", visibleIf: "{задаволенасць} >= 4" }, { тып: "каментарый", імя: "паляпшэннеВодгук", visibleIf: "{задаволенасць} <= 2" } ] }, { імя: "агляд", visibleIf: "{total} >= 100", elements: [] } ]};
Параўнайце на імгненне з версіяй RHF.
Блок superRefine, які ўмоўна патрабаваў імя карыстальніка і пароль, знік. visibleIf: "{hasAccount} = 'Yes'" у спалучэнні з isRequired: true апрацоўвае абедзве праблемы разам, на самім полі, дзе вы чакаеце іх знайсці. Ланцужок useWatch + useMemo, які вылічваў прамежкавы вынік, падатак і агульную суму, заменены трыма палямі выразаў, якія адносяцца да аднаго па імені. Умова старонкі агляду, якую ў версіі RHF можна было аднавіць толькі шляхам адсочвання праз showSubmit, этап 3 візуалізацыі. І, нарэшце, логіка кнопкі навігацыі - гэта адна ўласцівасць visibleIf аб'екта старонкі.
Такая ж логіка. Проста схема дае яму месца для пражывання, дзе ён бачны асобна, а не раскіданы па кампаненце. Акрамя таго, звярніце ўвагу, што схема выкарыстоўвае type: 'expression' для прамежкавых вынікаў, падаткаў і агульных вынікаў. Выраз прызначаны толькі для чытання і выкарыстоўваецца ў асноўным для адлюстравання вылічаных значэнняў. SurveyJS таксама падтрымлівае type: 'html' для статычнага змесціва, але для вылічаных значэнняў правільны выбар - выраз. Зараз для боку React. Аказанне і прадстаўленне Вельмі просты. Падключыце onComplete да вашага API такім жа чынам — праз useMutation або звычайную выбарку:
імпарт { useState, useEffect, useRef } з "react"; імпарт { useMutation } з "@tanstack/react-query"; імпарт { Model } з "survey-core"; імпарт { Survey } з "survey-react-ui"; імпарт "survey-core/survey-core.css";
функцыя экспарту SurveyForm() {const [model] = useState(() => новая мадэль(surveySchema));
мутацыя const = useMutation({ mutationFn: async (дадзеныя) => { 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 mutationRef = useRef(мутацыя); mutationRef.current = мутацыя; useEffect(() => { const handler = (sender) => mutationRef.current.mutate(sender.data); model.onComplete.add(handler); return () => model.onComplete.remove(handler); }, [model]); // ref пазбягае перарэгістрацыі апрацоўшчыка пры кожнай візуалізацыі (змены ідэнтычнасці аб'екта мутацыі)
вяртанне (
<>
Глядзіце Pen SurveyJS-03-SurveyJS [forked] ад sixthextinction.
onComplete спрацоўвае, калі карыстальнік даходзіць да канца апошняй бачнай старонкі. Такім чынам, калі агульная колькасць ніколі не перавышае 100 і старонка агляду прапускаецца, яна ўсё роўна спрацоўвае правільна, таму што SurveyJS ацэньвае бачнасць, перш чым вырашыць, што азначае «апошняя старонка». Затым sender.data змяшчае ўсе адказы разам з разлічанымі значэннямі (прамежкавы вынік, падатак, агулам) у якасці палёў першага класа, таму карысная нагрузка API ідэнтычная таму, што версія RHF сабрана ўручную ў onSubmit. TheШаблон mutationRef - гэта той самы шаблон, да якога вы б пацягнуліся ўсюды, дзе вам патрэбен стабільны апрацоўшчык падзей са значэннем, якое змяняецца пры кожным візуалізацыі - нічога асаблівага для SurveyJS.
Кампанент React больш не ўтрымлівае бізнес-логікі. Тут няма useWatch, няма ўмоўнага JSX, няма лічыльніка крокаў, няма ланцужка useMemo, няма superRefine. React робіць тое, у чым ён насамрэч добры: візуалізуе кампанент і падключае яго да выкліку API. Што выйшла з React?
Занепакоенасць РВЧ стэк SurveyJS Бачнасць Галіны JSX visibleIf Вытворныя значэнні useWatch / useMemo выраз Правілы крос-поля superRefine Умовы схемы Навігацыя крок стан Старонка бачнаяIf Размяшчэнне правілы Размяркоўваецца па файлах Цэнтралізаваны ў схеме
Што застаецца ў React, так гэта макет, стылізацыя, падключэнне і інтэграцыя прыкладанняў, то бок тое, для чаго React насамрэч створаны. Усё астатняе перанесена ў схему, і паколькі схема з'яўляецца проста аб'ектам JSON, яе можна захоўваць у базе даных, з версіямі незалежна ад кода вашага прыкладання або рэдагаваць з дапамогай унутраных інструментаў без неабходнасці разгортвання. Менеджэр прадукту, якому неабходна змяніць парог, які запускае старонку агляду, можа зрабіць гэта, не дакранаючыся кампанента. Гэта значная аперацыйная розніца для каманд, дзе паводзіны формы часта развіваюцца і не заўсёды кіруюцца інжынерамі. Калі выкарыстоўваць кожны з падыходаў? Вось добрае эмпірычнае правіла, якое працуе для мяне: уявіце, што вы цалкам выдалілі форму. Што б вы страцілі?
Калі гэта экраны, вам патрэбны формы, якія кіруюцца кампанентамі. Калі гэта бізнес-логіка, напрыклад, парогі, правілы разгалінавання і ўмоўныя патрабаванні, якія кадуюць рэальныя рашэнні, вам патрэбны механізм схемы.
Сапраўды гэтак жа, калі змены, якія вас чакаюць, тычацца ў асноўным цэтлікаў, палёў і макета, RHF вам добра падыдзе. Калі яны тычацца ўмоў, вынікаў і правілаў, якія вашай аператыўнай або юрыдычнай групе можа спатрэбіцца скарэктаваць днём у аўторак без падачы заявы, мадэль схемы з SurveyJS з'яўляецца больш сумленнай. Гэтыя два падыходы на самай справе не канкуруюць адзін з адным. Яны закранаюць розныя класы праблем, і памылка, якой варта пазбягаць, заключаецца ў неадпаведнасці абстракцыі вазе логікі — разглядаць сістэму правілаў як кампанент, таму што гэта знаёмы інструмент, або звяртацца да рухавіка палітыкі, таму што форма вырасла да трох крокаў і атрымала ўмоўнае поле. Форма, якую мы тут пабудавалі, наўмысна знаходзіцца побач з мяжой, дастаткова складаная, каб выявіць розніцу, але не настолькі экстрэмальная, каб параўнанне адчувалася сфальсіфікаваным. Большасць рэальных формаў, якія сталі грувасткімі ў вашай кодавай базе, верагодна, знаходзяцца побач з той самай мяжой, і пытанне звычайна заключаецца ў тым, ці назваў хто-небудзь тое, чым яны з'яўляюцца насамрэч. Выкарыстоўвайце React Hook Form + Zod, калі:
Формы арыентаваныя на CRUD; Логіка неглыбокая і кіруецца карыстацкім інтэрфейсам; Інжынеры валодаюць усімі паводзінамі; Бэкэнд застаецца крыніцай праўды.
Выкарыстоўвайце SurveyJS, калі:
Формы кадзіруюць бізнес-рашэнні; Правілы развіваюцца незалежна ад карыстацкага інтэрфейсу; Логіка павінна быць бачнай, праслухоўванай або версійнай; Неінжынеры ўплываюць на паводзіны; Адна і тая ж форма павінна працаваць на некалькіх інтэрфейсах.