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({ resolver: zodResolver(formSchema), defaultValues: { price: 0, quantity: 1, taxRate: 0.1, satisfaction: 3, hasAccount: "No", }, const price = useWatch({kontrol, pangalan: "presyo" }); const quantity = useWatch({ control, name: "quantity" }); const taxRate = useWatch({ control, name: "taxRate" }); const hasAccount = useWatch({ control, name: "hasAccount" }); const satisfaction = useWatch({ control, name: "satisfaction" }); const subtotal = useMemo(() => (presyo ?? 0) * (dami ?? 1), [presyo, dami]); const tax = useMemo(() => subtotal * (taxRate ?? 0), [subtotal, taxRate]); const total = useMemo(() => subtotal + buwis, [subtotal, buwis]); const onSubmit = (data: FormData) => mutation.mutate({ ...data, subtotal, buwis, kabuuan }); const showSubmit = (hakbang === 2 && kabuuan < 100) || (hakbang === 3 && kabuuan >= 100)

return (

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

{step === 1 && ( <>

Subtotal: {subtotal}
Tax: {tax}
Total: {total}
)}

{step === 2 && ( <>

{hasAccount === "Yes" && ( <> )}

{kasiyahan >= 4 && (