Ang artikulong ito ay itinataguyod ng SurveyJS Mayroong mental model na ibinabahagi ng karamihan sa mga developer ng React nang hindi ito tinatalakay nang malakas. Ang mga form na iyon ay palaging dapat na mga bahagi. Nangangahulugan ito ng isang stack tulad ng:
React Hook Form para sa lokal na estado (minimal re-render, ergonomic field registration, imperative interaction). Zod para sa validation (input correctness, boundary validation, type-safe parsing). React Query para sa backend: pagsusumite, muling pagsubok, pag-cache, pag-sync ng server, at iba pa.
At para sa karamihan ng mga form — ang iyong mga screen sa pag-login, ang iyong mga pahina ng mga setting, ang iyong mga CRUD modals — ito ay talagang gumagana. Ginagawa ng bawat piraso ang trabaho nito, malinis ang kanilang komposisyon, at maaari kang magpatuloy sa mga bahagi ng iyong aplikasyon na aktwal na nagpapaiba sa iyong produkto. Ngunit paminsan-minsan, ang isang form ay nagsisimulang mag-ipon ng mga bagay tulad ng mga panuntunan sa visibility na nakadepende sa mga naunang sagot, o nagmula sa mga value na dumadaloy sa tatlong field. Marahil kahit na ang buong mga pahina na dapat laktawan o ipakita batay sa isang kabuuang tumatakbo. Pinangangasiwaan mo ang unang kondisyon na may useWatch at isang inline na sangay, na ayos lang. Tapos isa pa. Pagkatapos ay inaabot mo ang superRefine para mag-encode ng mga cross-field na panuntunan na hindi maipahayag ng iyong Zod schema sa normal na paraan. Pagkatapos, ang hakbang na nabigasyon ay magsisimulang mag-leak ng lohika ng negosyo. Sa isang punto, titingnan mo kung ano ang iyong binuo at napagtanto na ang form ay hindi na talaga UI. Ito ay higit pa sa isang proseso ng pagpapasya, at ang puno ng bahagi ay kung saan mo lang ito naiimbak. Dito sa tingin ko ang mental model para sa mga form sa React ay nasira, at talagang walang kasalanan. Ang RHF + Zod stack ay mahusay sa kung para saan ito idinisenyo. Ang isyu ay madalas nating gamitin ito lampas sa punto kung saan tumutugma ang mga abstraction nito sa problema dahil ang alternatibo ay nangangailangan ng ibang paraan ng pag-iisip tungkol sa mga form nang buo. Ang artikulong ito ay tungkol sa alternatibong iyon. Para ipakita ito, bubuo kami ng eksaktong parehong multi-step na form nang dalawang beses:
Gamit ang React Hook Form + Zod wired sa React Query para sa pagsusumite, Sa SurveyJS, na tinatrato ang isang form bilang data — isang simpleng JSON schema — sa halip na isang component tree.
Parehong mga kinakailangan, parehong conditional logic, parehong API call sa dulo. Pagkatapos ay imamapa namin nang eksakto kung ano ang lumipat at kung ano ang nanatili, at maglalatag ng isang praktikal na paraan upang magpasya kung aling modelo ang dapat mong gamitin, at kailan. Ang form na aming binubuo:
Ang form na ito ay gagamit ng 4 na hakbang na daloy: Hakbang 1: Mga Detalye
Pangalan (kinakailangan), Email (kinakailangan, wastong format).
Hakbang 2: Mag-order
Presyo ng unit, dami, rate ng buwis, Hinango: Subtotal, buwis, Kabuuan.
Hakbang 3: Account at Feedback
May account ka ba? (Oo/Hindi) Kung Oo → username + password, parehong kinakailangan. Kung Hindi → nakolekta na ang email sa hakbang 1.
Rating ng kasiyahan (1–5) Kung ≥ 4 → itanong ang “Ano ang nagustuhan mo?” Kung ≤ 2 → itanong ang “Ano ang maaari nating pagbutihin?”
Hakbang 4: Suriin
Lumalabas lamang kung kabuuan >= 100 Panghuling pagsusumite.
Hindi naman ito extreme. Ngunit sapat na upang ilantad ang mga pagkakaiba sa arkitektura. Bahagi 1: Dahil sa Component (React Hook Form + Zod) Pag-install npm install react-hook-form zod @hookform/resolvers @tanstack/react-query
Zod Schema Magsimula tayo sa Zod schema, dahil kadalasan doon nabubuo ang hugis ng form. Para sa unang dalawang hakbang — mga personal na detalye at mga input ng order — lahat ay diretso: kinakailangang mga string, mga numerong may mga minimum, at isang enum. Magsisimula ang kawili-wiling bahagi kapag sinubukan mong ipahayag ang mga kondisyong tuntunin.
import { z } mula sa "zod";
i-export ang const formSchema = z.object({ firstName: z.string().min(1, "Required"), email: z.string().email("Invalid email"), presyo: z.number().min(0), quantity: z.number().min(1), taxRate: z.number(), hasAccount: z.enumNo. password: z.string().optional(), kasiyahan: z.number().min(1).max(5), positiveFeedback: z.string().optional(), improvementFeedback: z.string().optional(),}).superRefine((data, ctx) => { if (data.hasAccount === "Yes") { if (!data.username) { stom(!data.username) { s. ["username"], mensahe: "Kinakailangan" }); } if (!data.password || data.password.length < 6) { ctx.addIssue({ code: "custom", path: ["password"], message: "Min 6 na character" } }
kung (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ code: "custom", path: ["positiveFeedback"], mensahe: "Pakibahagi kung ano ang nagustuhan mo" }); }
if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ code: "custom", path:["improvementFeedback"], mensahe: "Pakisabi sa amin kung ano ang dapat pagbutihin" }); }});
uri ng pag-export FormData = z.infer
Pansinin na ang username at password ay nai-type bilang opsyonal() kahit na kinakailangan ang mga ito dahil inilalarawan ng schema ng antas ng uri ng Zod ang hugis ng bagay, hindi ang mga panuntunang namamahala kapag mahalaga ang mga field. Ang kondisyong kinakailangan ay kailangang mabuhay sa loob ng superRefine, na tumatakbo pagkatapos ma-validate ang hugis at may access sa buong bagay. Ang paghihiwalay na iyon ay hindi isang kapintasan; ito lang ang idinisenyo ng tool: ang superRefine ay kung saan napupunta ang cross-field logic kapag hindi ito maipahayag sa mismong istraktura ng schema. Ang kapansin-pansin din dito ay kung ano ang hindi ipinapahayag ng schema na ito. Wala itong konsepto ng mga pahina, walang konsepto kung aling mga field ang makikita sa puntong iyon, at walang konsepto ng nabigasyon. Lahat ng iyon ay mabubuhay sa ibang lugar. Bahagi ng Form
import { useForm, useWatch } mula sa "react-hook-form";import { zodResolver } mula sa "@hookform/resolvers/zod";import {useMutation } mula sa "@tanstack/react-query";import {useState, useMemo } mula sa "react";import { formSchema, type./schema} mula sa " FormData }
const STEPS = ["detalye", "order", "account", "review"];
i-type ang OrderPayload = FormData & { subtotal: number; buwis: numero; kabuuan: numero };
export function RHFMultiStepForm() { const [step, setStep] = useState(0);
const mutation = useMutation({ mutationFn: async (payload: OrderPayload) => { const res = await fetch("/api/orders", { paraan: "POST", mga header: { "Content-Type": "application/json" }, katawan: JSON.stringify(payload), }); if (!res.ok) throw new Error("Failed to submit"); return res.json(); }, });
const { register, control, handleSubmit, formState: { errors }, } = useForm
return (
);}Tingnan ang Pen SurveyJS-03-RHF [na-forked] ng sixthextinction. Napakaraming nangyayari dito, at sulit na maghinay-hinay upang mapansin kung saan napunta ang mga bagay.
Ang mga nakuhang halaga — subtotal, buwis, kabuuan — ay kinukuwenta sa bahagi sa pamamagitan ng useWatch at useMemo dahil nakadepende ang mga ito sa mga live na value ng field at walang ibang natural na lugar para sa kanila. Ang mga panuntunan sa visibility para sa username, password, positiveFeedback, at improvementFeedback ay live sa JSX bilang mga inline na kondisyon. Ang lohika ng paglaktaw sa hakbang — lumalabas lang ang pahina ng pagsusuri kapag ang kabuuan >= 100 — ay naka-embed sa variable ng showSubmit at ang kundisyon ng pag-render sa hakbang 3. Ang mismong nabigasyon ay isang useState counter lamang na manu-mano naming dinaragdagan. Pinangangasiwaan ng React Query ang mga muling pagsubok, pag-cache, at pagpapawalang-bisa. Tinatawag lang ng form ang mutation.mutate na may validated na data.
Wala sa mga ito ang mali, per se. Ito ay idiomatic React pa rin, at ang bahagi ay lubos na gumaganap salamat sa kung paano muling nagre-render ang RHF isolates. Ngunit kung ibibigay mo ito sa isang taong hindi pa nagsulat nito at hilingin sa kanila na ipaliwanag sa ilalim ng kung anong mga kundisyon ang lalabas sa pahina ng pagsusuri, kailangan nilang i-trace sa pamamagitan ng showSubmit, ang step 3 render na kundisyon, at ang nav button na logic — tatlong magkahiwalay na lugar — upang muling buuin ang isang panuntunan na maaaring nakasaad sa isang linya. Gumagana ang form, oo, ngunit hindi talaga nasusuri ang pag-uugali bilang isang sistema. Dapat itong isagawa sa pag-iisip. Higit sa lahat, ang pagbabago nito ay nangangailangan ng paglahok sa engineering. Kahit na ang isang maliit na pag-tweak, tulad ng pagsasaayos kapag lumabas ang hakbang sa pagsusuri, ay nangangahulugan ng pag-edit sa bahagi, pag-update ng pagpapatunay, pagbubukas ng pull request, paghihintay para sa pagsusuri, at pag-deploy muli. Bahagi 2: Batay sa Schema (SurveyJS) Ngayon, buuin natin ang parehong daloy gamit ang isang schema. Pag-install npm i-install ang survey-core survey-react-ui @tanstack/react-query
survey-coreAng MIT-licensed platform-independent runtime engine na nagpapagana sa pag-render ng form ng SurveyJS — ang bahaging pinapahalagahan namin dito. Nangangailangan ito ng JSON schema, bubuo ng panloob na modelo mula rito, at pinangangasiwaan ang lahat ng bagay na maaaring mabuhay sa iyong bahagi ng React: pagsusuri sa mga expression ng visibility, pag-compute ng mga derived value, pamamahala sa status ng page, pagsubaybay sa validation, at pagpapasya kung ano ang ibig sabihin ng "kumpleto" kung aling mga page ang aktwal na ipinakita.
survey-react-uiAng UI / rendering layer na nagkokonekta sa modelong iyon sa React. Ito ay mahalagang bahagi ng
Magkasama, binibigyan ka nila ng ganap na gumagana, multi-page na runtime na form nang hindi nagsusulat ng isang linya ng kontrol na daloy. Ang mismong format ng schema ay, gaya ng sinabi noon, isang JSON lamang — walang DSL o anumang pagmamay-ari. Maaari mo itong i-inline, i-import ito mula sa isang file, kunin ito mula sa isang API, o iimbak ito sa isang column ng database at i-hydrate ito sa runtime. Ang Parehong Form, Bilang Data Narito ang parehong anyo, sa pagkakataong ito ay ipinahayag bilang JSON object. Tinutukoy ng schema ang lahat: istraktura, pagpapatunay, mga panuntunan sa visibility, mga kalkulasyon na hinango, pag-navigate sa page — at ibibigay ito sa isang Modelo na sinusuri ito sa runtime. Narito kung ano ang hitsura nito nang buo:
export const surveySchema = { title: "Order Flow", showProgressBar: "top", mga pahina: [ { name: "details", elements: [ { type: "text", name: "firstName", isRequired: true }, { type: "text", name: "email", inputType: "email", isRequired: true", type: "validators" }] } ] }, { name: "order", elemento: [ { type: "text", name: "price", inputType: "number", defaultValue: 0 }, { type: "text", name: "quantity", inputType: "number", defaultValue: 1 }, { type: "dropdown",name: "taxRate", defaultValue: 0.1, mga pagpipilian: [ { value: 0.05, text: "5%" }, { value: 0.1, text: "10%" }, { value: 0.15, text: "15%" } ] }, { type: "expression", name: "{price"; "expression", pangalan: "tax", expression: "{subtotal} {taxRate}" }, { type: "expression", name: "total", expression: "{subtotal} + {tax}" } ] }, { name: "account", mga elemento: [ { type: "radiogroup", name: "hasAccount", mga pagpipilian: "Kung Oo}", "No type" "{hasAccount} = 'Yes'", isRequired: true }, { type: "text", name: "password", inputType: "password", visibleIf: "{hasAccount} = 'Yes'", isRequired: true, validators: [{ type: "text", minLength: 6, text: "Min 6 na characters": {type}: "tis: rate ng min", {type}: "fast name" 1, rateMax: 5 }, { type: "comment", name: "positiveFeedback", visibleIf: "{satisfaction} >= 4" }, { type: "comment", name: "improvementFeedback", visibleIf: "{satisfaction} <= 2" } ] }, { name: "{", visible If ]};
Ihambing ito sa bersyon ng RHF nang ilang sandali.
Wala na ang superRefine block na may kondisyong kinakailangan ng username at password. visibleIf: "{hasAccount} = 'Yes'" na sinamahan ng isRequired: pinangangasiwaan ng true ang parehong mga alalahanin, sa mismong field, kung saan mo inaasahan na mahahanap ang mga ito. Ang chain ng useWatch + useMemo na nag-compute ng subtotal, buwis, at kabuuan ay pinapalitan ng tatlong field ng expression na tumutukoy sa isa't isa ayon sa pangalan. Ang kundisyon ng pahina ng pagsusuri, na sa bersyon ng RHF ay nabubuo lamang sa pamamagitan ng pagsubaybay sa showSubmit, ang sangay ng pag-render ng hakbang 3. At sa wakas, ang logic ng nav button ay isang solong visibleIf na property sa object ng page.
Ang parehong lohika ay naroroon. Binibigyan lang ito ng schema ng lugar na tirahan kung saan nakikita ito nang nakahiwalay, sa halip na kumalat sa bahagi. Gayundin, tandaan na ang schema ay gumagamit ng uri: 'expression' para sa subtotal, buwis, at kabuuan. Ang expression ay read-only at pangunahing ginagamit upang ipakita ang mga kalkuladong halaga. Sinusuportahan din ng SurveyJS ang uri: 'html' para sa static na nilalaman, ngunit para sa mga kinakalkula na halaga, ang expression ay ang tamang pagpipilian. Ngayon para sa React side. Pag-render at Pagsusumite Napakasimple. Wire onComplete sa iyong API sa parehong paraan — sa pamamagitan ng useMutation o plain fetch:
import { useState, useEffect, useRef } mula sa "react";import { useMutation } mula sa "@tanstack/react-query";import { Model } mula sa "survey-core";import { Survey } mula sa "survey-react-ui";import "survey-core/survey-core.css";
export function SurveyForm() { const [model] = useState(() => bagong Model(surveySchema));
const mutation = useMutation({ mutationFn: async (data) => { const res = await fetch("/api/orders", { paraan: "POST", mga header: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) throw new Error("Failed to submit"); return res.json(); }, });
const mutationRef = useRef(mutation); mutationRef.current = mutation; useEffect(() => { const handler = (sender) => mutationRef.current.mutate(sender.data);model.onComplete.add(handler); return () => model.onComplete.remove(handler); }, [model]); // iniiwasan ng ref ang muling pagrehistro ng handler sa bawat pag-render (nagbabago ang pagkakakilanlan ng object ng mutation)
bumalik (
<>
Tingnan ang Pen SurveyJS-03-SurveyJS [na-forked] ng sixthextinction.
Ang onComplete ay gagana kapag naabot ng user ang dulo ng huling nakikitang page. Kaya't kung ang kabuuan ay hindi kailanman lalampas sa 100 at ang pahina ng pagsusuri ay nilaktawan, gagana pa rin ito nang tama dahil sinusuri ng SurveyJS ang visibility bago magpasya kung ano ang ibig sabihin ng "huling pahina." Pagkatapos, naglalaman ang sender.data ng lahat ng sagot kasama ang mga kinakalkula na halaga (subtotal, buwis, kabuuan) bilang mga first-class na field, kaya ang payload ng API ay kapareho ng kung ano ang manual na binuo ng bersyon ng RHF sa onSubmit. AngAng pattern ng mutationRef ay ang parehong maaabot mo kahit saan kailangan mo ng isang matatag na tagapangasiwa ng kaganapan sa isang halaga na nagbabago sa bawat pag-render — walang partikular na SurveyJS tungkol dito.
Ang React component ay hindi na naglalaman ng anumang lohika ng negosyo. Walang useWatch, walang conditional JSX, walang step counter, walang useMemo chain, walang superRefine. Ginagawa ng React kung ano ang talagang mahusay sa: pag-render ng isang bahagi at pag-wire nito sa isang tawag sa API. Ano ang Inilipat sa Reaksyon?
Pag-aalala RHF Stack SurveyJS Visibility Mga sangay ng JSX nakikitaKung Mga halagang hinango gamitinManood / gumamit ngMemo pagpapahayag Mga panuntunan sa cross-field superRefine Mga kondisyon ng schema Pag-navigate estado ng hakbang Nakikita ang pahina kung Lokasyon ng panuntunan Ibinahagi sa mga file Sentralisado sa schema
Ang nananatili sa React ay ang layout, styling, submission wiring, at app integration, ibig sabihin, ang mga bagay na talagang idinisenyo ng React. Lahat ng iba pa ay inilipat sa schema, at dahil ang schema ay isang JSON object lang, maaari itong iimbak sa isang database, independiyenteng bersyon ng iyong application code, o i-edit sa pamamagitan ng panloob na tooling nang hindi nangangailangan ng pag-deploy. Maaaring gawin iyon ng isang product manager na kailangang baguhin ang threshold na nagti-trigger sa page ng pagsusuri nang hindi hinahawakan ang bahagi. Iyon ay isang makabuluhang pagkakaiba sa pagpapatakbo para sa mga koponan kung saan ang pag-uugali ng anyo ay madalas na nagbabago at hindi palaging hinihimok ng mga inhinyero. Kailan Gagamitin ang Bawat Diskarte? Narito ang isang magandang tuntunin ng thumb na gumagana para sa akin: isipin na ganap na tanggalin ang form. Ano ang mawawala sa iyo?
Kung ito ay mga screen, gusto mo ng mga form na hinimok ng bahagi. Kung ito ay lohika ng negosyo, tulad ng mga threshold, sumasanga na mga panuntunan, at mga kinakailangan sa kondisyon na nag-encode ng mga tunay na desisyon, gusto mo ng schema engine.
Katulad nito, kung ang mga pagbabagong darating sa iyo ay kadalasang tungkol sa mga label, field, at layout, magsisilbi sa iyo nang maayos ang RHF. Kung ang mga ito ay tungkol sa mga kundisyon, kinalabasan, at panuntunan na maaaring kailanganin ng iyong ops o legal na team na ayusin sa hapon ng Martes nang hindi nag-file ng ticket, ang modelo ng schema na may SurveyJS ang mas tapat na akma. Ang dalawang approach na ito ay hindi talaga sa kompetisyon sa isa't isa. Tinutugunan nila ang iba't ibang klase ng mga problema, at ang pagkakamaling dapat iwasan ay ang hindi pagtutugma ng abstraction sa bigat ng lohika — pagtrato sa isang sistema ng panuntunan bilang isang bahagi dahil iyon ang pamilyar na tool, o pag-abot para sa isang makina ng patakaran dahil ang isang form ay lumago sa tatlong hakbang at nakakuha ng isang kondisyong larangan. Ang form na itinayo namin dito ay kusa na nakaupo malapit sa hangganan, sapat na kumplikado upang ilantad ang pagkakaiba ngunit hindi masyadong sukdulan na ang paghahambing ay parang niloko. Karamihan sa mga tunay na anyo na naging mahirap gamitin sa iyong codebase ay malamang na nakaupo malapit sa parehong hangganan, at ang tanong ay karaniwang kung may pinangalanan kung ano talaga sila. Gamitin ang React Hook Form + Zod kapag:
Ang mga form ay CRUD-oriented; Ang lohika ay mababaw at UI-driven; Pagmamay-ari ng mga inhinyero ang lahat ng pag-uugali; Ang backend ay nananatiling pinagmumulan ng katotohanan.
Gamitin ang SurveyJS kapag:
Mga form na nag-encode ng mga desisyon sa negosyo; Ang mga panuntunan ay umuunlad nang hiwalay sa UI; Ang lohika ay dapat na nakikita, naa-audit, o may bersyon; Ang mga hindi inhinyero ay nakakaimpluwensya sa pag-uugali; Ang parehong form ay dapat tumakbo sa maraming frontend.