Ezt a cikket a SurveyJS szponzorálja Van egy mentális modell, amelyet a legtöbb React fejlesztő megoszt anélkül, hogy hangosan megvitatná. A formáknak mindig összetevőknek kell lenniük. Ez egy olyan halmot jelent, mint:
React Hook Form helyi állapothoz (minimális újrarenderelés, ergonómikus mezőregisztráció, kötelező interakció). Zod az érvényesítéshez (bevitel helyessége, határellenőrzés, típusbiztos elemzés). React Query a háttérrendszerhez: benyújtás, újrapróbálkozások, gyorsítótár, szerver szinkronizálás stb.
És az űrlapok túlnyomó többségénél – a bejelentkezési képernyők, a beállítási oldalak, a CRUD módok – ez nagyon jól működik. Mindegyik darab elvégzi a dolgát, tisztán komponál, és továbbléphet az alkalmazás azon részeire, amelyek ténylegesen megkülönböztetik termékét. Időnként azonban egy űrlap olyan dolgokat kezd felhalmozni, mint például a láthatósági szabályok, amelyek a korábbi válaszoktól függenek, vagy a származtatott értékek, amelyek három mezőn keresztül kaszkádoznak. Esetleg akár teljes oldalak is, amelyeket ki kellene hagyni vagy megjeleníteni a futó összesítés alapján. Az első feltételt egy useWatch-el és egy inline ággal kezeled, ami rendben van. Aztán egy másik. Ezután a superRefine-hez nyúlsz, hogy kódoljon olyan keresztmezős szabályokat, amelyeket a Zod-sémád nem tud a szokásos módon kifejezni. Ezután a lépéses navigáció üzleti logikát kezd kiszivárogtatni. Egy ponton megnézed, hogy mit építettél fel, és rájössz, hogy az űrlap már nem igazán UI. Ez inkább egy döntési folyamat, és az összetevőfa pont ott van, ahol véletlenül tárolta. Szerintem itt omlik meg a React formák mentális modellje, és ez valójában senki hibája. Az RHF + Zod verem kiváló ahhoz, amire tervezték. A probléma az, hogy hajlamosak vagyunk tovább használni azt a pontot, ahol absztrakciói megfelelnek a problémának, mert az alternatíva teljesen másfajta gondolkodást igényel a formákról. Ez a cikk erről az alternatíváról szól. Ennek bemutatásához pontosan ugyanazt a többlépcsős űrlapot készítjük el kétszer:
A React Hook Form + Zod bekötéssel a React Queryhez a benyújtáshoz, A SurveyJS-szel, amely az űrlapot adatként – egyszerű JSON-sémaként – kezeli, nem pedig komponensfaként.
Ugyanazok a követelmények, ugyanaz a feltételes logika, ugyanaz az API-hívás a végén. Ezután feltérképezzük, hogy pontosan mi mozdult el és mi maradt, és gyakorlati módszert dolgozunk ki annak eldöntésére, hogy melyik modellt és mikor érdemes használni. Az általunk készített forma:
Ez az űrlap 4 lépésből álló folyamatot használ: 1. lépés: Részletek
Keresztnév (kötelező), E-mail (kötelező, érvényes formátum).
2. lépés: Rendelés
Egységár, Mennyiség, adókulcs, Származtatott: Részösszeg, adó, Összesen.
3. lépés: Fiók és visszajelzés
Van fiókod? (Igen/Nem) Ha Igen → felhasználónév + jelszó, mindkettő kötelező. Ha nem → az 1. lépésben már összegyűjtöttük az e-mailt.
Elégedettségi értékelés (1-5) Ha ≥ 4 → kérdezd meg: „Mi tetszett?” Ha ≤ 2 → kérdezd meg: „Miben javíthatunk?”
4. lépés: Tekintse át
Csak akkor jelenik meg, ha összesen >= 100 Végső benyújtás.
Ez nem extrém. De elég feltárni az építészeti különbségeket. 1. rész: Alkatrész-vezérelt (React Hook Form + Zod) Telepítés npm install react-hook-form zod @hookform/resolvers @tanstack/react-query
Zod séma Kezdjük a Zod sémával, mert általában ott alakul ki a forma alakja. Az első két lépésben – a személyes adatok és a rendelés bevitele – minden egyértelmű: kötelező karakterláncok, számok minimumokkal és egy enum. Az érdekes rész akkor kezdődik, amikor megpróbálod kifejezni a feltételes szabályokat.
import { z } a "zod"-ból;
export const formSchema = z.object({ keresztnév: z.string().min(1, "Kötelező"), email: z.string().email("Érvénytelen e-mail"), ár: z.szám().min(0), mennyiség: z.szám().min(1), taxRate: z.number(), hasAccount: ["]esum",("]esum), a (!data.username) { ctx.addIssue({ kód: "egyéni", elérési út: ["felhasználónév"], üzenet: "Kötelező" }); } if (!data.password || data.password.length < 6) { ctx.addIssue({ kód: "egyéni jelszó" }); } }
if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ kód: "egyéni", elérési út: ["pozitívFeedback"], üzenet: "Oszd meg, mi tetszett" }); }
if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ kód: "egyéni", elérési út:["improvementFeedback"], üzenet: "Kérjük, mondja el nekünk, mit kell javítani" }); }});
export típusa FormData = z.infer
Figyelje meg, hogy a felhasználónév és a jelszó opcionális()-ként van beírva, még akkor is, ha feltételesen kötelező, mert a Zod típusszintű sémája az objektum alakját írja le, nem pedig a mezők fontosságát szabályozó szabályokat. A feltételes követelménynek a superRefine-ben kell élnie, amely az alakzat érvényesítése után fut, és hozzáfér a teljes objektumhoz. Ez az elválasztás nem hiba; csak erre tervezték az eszközt: a superRefine az a hely, ahol a cross-field logika megy, amikor nem fejezhető ki magában a sémaszerkezetben. Ami itt is figyelemre méltó, az az, amit ez a séma nem fejez ki. Nincs fogalma az oldalakról, nincs fogalma arról, hogy mely mezők melyik ponton láthatók, és nincs fogalma a navigációról sem. Mindez máshol fog élni. Form komponens
import { useForm, useWatch } from "react-hook-form";import { zodResolver } from "@hookform/resolvers/zod";import { useMutation } from "@tanstack/react-query";import { useState, useMemo } from "react";import from "formm.tama"};
const STEPS = ["részletek", "megrendelés", "számla", "áttekintés"];
type OrderPayload = FormData & { részösszeg: szám; adó: szám; összesen: szám };
export function RHFMultiStepForm() { const [lépés, setStep] = useState(0);
const mutáció = useMutation({ mutationFn: async (rakomány: OrderPayload) => { const res = várja a fetch("/api/orders", { módszer: "POST", fejlécek: { "Content-Type": "application/json" }, törzs: JSON.stringify(payload), }); if (!res.ok) throw new Error("Nem sikerült elküldeni"); return res.json(); }, });
const { register, control, handleSubmit, formState: { errors }, } = useForm
return (
);}Lásd a Pen SurveyJS-03-RHF [forked] a hatodik extinkcióval. Elég sok minden történik itt, és érdemes lelassítani, hogy észrevegyük, hol végződtek a dolgok.
A származtatott értékeket – részösszeg, adó, végösszeg – a komponensben a useWatch és a useMemo segítségével számítjuk ki, mivel az élő mezőértékektől függenek, és nincs más természetes helyük. A felhasználónévre, jelszóra, pozitív visszajelzésre és fejlesztésre vonatkozó láthatósági szabályok a JSX-ben soron belüli feltételként jelennek meg. A lépésátugrási logika – az áttekintési oldal csak akkor jelenik meg, ha összesen >= 100 – be van ágyazva a showSubmit változóba és a megjelenítési feltételbe a 3. lépésben. Maga a navigáció csak egy useState számláló, amelyet manuálisan növelünk. A React Query kezeli az újrapróbálkozásokat, a gyorsítótárazást és az érvénytelenítést. Az űrlap csak a mutation.mutate-t hívja meg érvényesített adatokkal.
Önmagában ezek közül egyik sem rossz. Ez még mindig idiomatikus React, és a komponens meglehetősen teljesítőképes, köszönhetően annak, hogy az RHF hogyan izolálja a re-rendereket. De ha átadná ezt valakinek, aki nem írta meg, és megkérné, hogy magyarázza el, milyen feltételek mellett jelenik meg az áttekintési oldal, akkor a showSubmit, a 3. lépés renderelési feltétele és a navigációs gomb logikája – három különálló hely – segítségével vissza kell állítania egy olyan szabályt, amelyet egy sorban is megfogalmazhatott volna. A forma működik, igen, de a viselkedés rendszerként nem igazán ellenőrizhető. Mentálisan kell végrehajtani. Ennél is fontosabb, hogy megváltoztatása mérnöki közreműködést igényel. Még egy apró módosítás is, mint például a felülvizsgálati lépés megjelenésekor történő módosítás, az összetevő szerkesztését, az érvényesítés frissítését, a lekérési kérés megnyitását, a felülvizsgálatra várást és az újbóli telepítést jelenti. 2. rész: Sémavezérelt (SurveyJS) Most építsük fel ugyanazt a folyamatot egy séma segítségével. Telepítés npm install survey-core survey-react-ui @tanstack/react-query
survey-coreA MIT-licencű, platformfüggetlen futtatómotor, amely a SurveyJS űrlap-megjelenítését hajtja végre – ez a rész, amelyre itt számítunk. Ez egy JSON-sémát vesz fel, belső modellt épít belőle, és mindent kezel, ami egyébként a React komponensben élne: a láthatósági kifejezések kiértékelése, a származtatott értékek kiszámítása, az oldalállapot kezelése, az érvényesítés nyomon követése, valamint annak eldöntése, hogy a ténylegesen megjelenített oldalak alapján mit jelent a „befejezett”.
survey-react-uiAz a felhasználói felület / megjelenítési réteg, amely összeköti a modellt a React-tal. Lényegében egy
Együtt teljes mértékben működőképes, többoldalas űrlap-futási környezetet biztosítanak anélkül, hogy egyetlen sornyi vezérlési folyamatot írnának. Maga a sémaformátum, mint korábban említettük, csak egy JSON – nem tartalmaz DSL-t vagy bármi mást. Beépítheti, importálhatja fájlból, lekérheti egy API-ból, vagy tárolhatja egy adatbázis oszlopban, és futás közben hidratálhatja. Ugyanaz a forma, mint az adat Itt ugyanaz az űrlap, ezúttal JSON-objektumként kifejezve. A séma mindent meghatároz: szerkezetet, érvényesítést, láthatósági szabályokat, származtatott számításokat, oldalnavigációt – és átadja egy modellnek, amely futás közben kiértékeli. Így néz ki teljes egészében:
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, in email, id: }, ] }, { name: "order", elements: [ { type: "text", name: "price", inputType: "number", defaultValue: 0 }, { type: "text", name: "quantity", inputType: "number", defaultValue: 1 }, { type: "dropdown",név: "taxRate", alapértelmezettÉrték: 0,1, választási lehetőségek: [ { érték: 0,05, szöveg: "5%" }, { érték: 0,1, szöveg: "10%" }, { érték: 0,15, szöveg: "15%" } ] }, { típus: "kifejezés", név: "részösszeg"}} kifejezés { nt{price} "kifejezés", név: "adó", kifejezés: "{subtotal} {taxRate}" }, { típus: "kifejezés", név: "összesen", kifejezés: "{subtotal} + {tax}" } } }, { name: "account", elemek: [ { type: "radiogroup", name: "hasAccount", név: "hasAccount", szöveg, "nem" típus}, "{:"] "felhasználónév", nähtavIf: "{hasAccount} = 'Igen'", isKötelező: igaz }, { típus: "szöveg", név: "jelszó", inputType: "jelszó", láthatóIf: "{hasAccount} = 'Igen'", isKötelező: igaz, érvényesítők: [{ típus: "szöveg ", min6 karakter, szöveg}th ] típus: "értékelés", név: "elégedettség", rateMin: 1, rateMax: 5 }, { type: "comment", név: "pozitívFeedback", láthatóIf: "{elégedettség} >= 4" }, { type: "comment", név: "javításFeedback", láthatóIf: "{elégedettség]2}nézet", láthatóIf: "{összesen} >= 100", elemek: [] } ]};
Hasonlítsa össze ezt egy pillanatra az RHF verzióval.
A feltételesen felhasználónevet és jelszót igénylő superRefine blokk eltűnt. nähtavIf: "{hasAccount} = 'Igen'" az isRequired paraméterrel kombinálva: a true mindkét problémát együtt kezeli, azon a területen, ahol várhatóan megtalálja őket. A részösszeget, adót és végösszeget kiszámító useWatch + useMemo láncot három kifejezésmező váltja fel, amelyek név szerint hivatkoznak egymásra. A felülvizsgálati oldal feltétele, amely az RHF verzióban csak a showSubmit, a 3. lépéses renderelési ág nyomon követésével volt rekonstruálható. Végül pedig a navigációs gomb logikája az oldalobjektum egyetlen LáthatóIf tulajdonsága.
Ugyanez a logika létezik. Csupán arról van szó, hogy a séma olyan helyet ad neki, ahol élhet, ahol elszigetelten látható, nem pedig szétszórva az összetevőn. Azt is vegye figyelembe, hogy a séma a következő típust használja: „kifejezés” a részösszeghez, az adóhoz és az összeghez. A kifejezés csak olvasható, és főként számított értékek megjelenítésére szolgál. A SurveyJS támogatja a 'html' típust is statikus tartalom esetén, de számított értékek esetén a kifejezés a megfelelő választás. Most pedig a React oldalról. Renderelés és benyújtás Nagyon egyszerű. Az onComplete-et ugyanúgy csatlakoztathatja az API-hoz – a useMutation vagy a plain fetch segítségével:
import { useState, useEffect, useRef } from "react";import { useMutation } from "@tanstack/react-query";import { Model } from "survey-core";import { Survey } from "survey-react-ui";import "survey-core/survey-core".
export function SurveyForm() { const [modell] = useState(() => new Model(surveySchema));
const mutáció = useMutation({ mutationFn: async (adatok) => { const res = várja a fetch("/api/orders", { módszer: "POST", fejlécek: { "Content-Type": "application/json" }, törzs: JSON.stringify(data), }); if (!res.ok) throw new Error("Nem sikerült elküldeni"); return res.json(); }, });
const mutációRef = useRef(mutáció); mutationRef.current = mutáció; useEffect(() => { const handler = (sender) => mutationRef.current.mutate(sender.data); model.onComplete.add(handler); return () => model.onComplete.remove(handler); }, [modell]); // ref elkerüli a kezelő újraregisztrálását minden renderelésnél (a mutációs objektum azonossága megváltozik)
vissza (
<>
Lásd a Pen SurveyJS-03-SurveyJS [forked] a hatodik extinkcióval.
Az onComplete akkor aktiválódik, amikor a felhasználó eléri az utolsó látható oldal végét. Tehát ha az összeg soha nem haladja meg a 100-at, és az áttekintési oldalt kihagyják, akkor is megfelelően aktiválódik, mert a SurveyJS kiértékeli a láthatóságot, mielőtt eldönti, mit jelent az „utolsó oldal”. Ezután a sender.data tartalmazza az összes választ a számított értékekkel együtt (részösszeg, adó, összesen) első osztályú mezőkként, így az API hasznos terhelése megegyezik azzal, amit az RHF-verzió manuálisan összeállított az onSubmitben. AA mutationRef minta ugyanaz, ahol bárhol elérne, ahol stabil eseménykezelőre van szüksége egy olyan érték felett, amely minden megjelenítéskor változik – semmi SurveyJS-specifikus.
A React összetevő már egyáltalán nem tartalmaz üzleti logikát. Nincs useWatch, nincs feltételes JSX, nincs lépésszámláló, nincs useMemo lánc, nincs superRefine. A React azt csinálja, amiben valójában jó: egy komponenst renderel, és egy API-híváshoz csatlakoztatja. Mi mozdult ki a reakcióból?
Aggodalom RHF Stack SurveyJS Láthatóság JSX ágak láthatóHa Származtatott értékek useWatch / useMemo kifejezést Cross-field szabályok szuperfinomítás Sémafeltételek Navigáció lépés állapota Oldal láthatóHa Szabály helye Fájlok között elosztva Központosítva a sémában
Ami a Reactban marad, az az elrendezés, a stílus, a benyújtási huzalozás és az alkalmazások integrációja, vagyis az, amire a Reactot valójában tervezték. Minden más átkerült a sémába, és mivel a séma csak egy JSON-objektum, tárolható egy adatbázisban, az alkalmazás kódjától függetlenül verziószámítható, vagy belső eszközzel szerkeszthető anélkül, hogy telepítésre lenne szükség. A termékmenedzser, akinek módosítania kell a felülvizsgálati oldalt kiváltó küszöbértéket, megteheti ezt anélkül, hogy megérintené az összetevőt. Ez jelentős működési különbség a csapatok számára, ahol a formai viselkedés gyakran változik, és nem mindig a mérnökök irányítják. Mikor kell alkalmazni az egyes megközelítéseket? Íme egy jó hüvelykujjszabály, ami működik számomra: képzelje el, hogy teljesen törli az űrlapot. Mit veszítenél?
Ha képernyőkről van szó, akkor komponensvezérelt űrlapokat szeretne. Ha az üzleti logika, például a küszöbértékek, az elágazási szabályok és a feltételes követelmények valós döntéseket kódolnak, akkor sémamotort szeretne.
Hasonlóképpen, ha az Ön felé érkező változtatások főként címkékre, mezőkre és elrendezésre vonatkoznak, az RHF jól fog szolgálni. Ha olyan feltételekről, eredményekről és szabályokról van szó, amelyeket az operátornak vagy a jogi csapatnak módosítania kell egy kedd délutáni jegy benyújtása nélkül, akkor a SurveyJS sémamodellje a legőszintébb. Ez a két megközelítés nem igazán versenyez egymással. Különböző problémaosztályokkal foglalkoznak, és az a hiba, amelyet érdemes elkerülni, az, hogy az absztrakciót nem illesztjük össze a logika súlyával – egy szabályrendszert komponensként kezelünk, mert az ismerős eszköz, vagy egy irányelvmotorhoz nyúlunk, mert egy űrlap három lépésre nőtt, és feltételes mezőt kapott. Az általunk itt felépített forma szándékosan a határ közelében helyezkedik el, elég összetett ahhoz, hogy feltárja a különbséget, de nem olyan szélsőséges, hogy az összehasonlítás eltorzultnak tűnjön. A legtöbb valódi forma, amely nehézkessé vált a kódbázisodban, valószínűleg ugyanannak a határnak a közelében található, és a kérdés általában csak az, hogy megnevezte-e valaki, hogy valójában mi is ők. Használja a React Hook Form + Zod-ot, ha:
Az űrlapok CRUD-orientáltak; A logika sekélyes és UI-vezérelt; Minden viselkedés a mérnököké; A háttér továbbra is az igazság forrása.
Használja a SurveyJS-t, ha:
Az űrlapok üzleti döntéseket kódolnak; A szabályok a felhasználói felülettől függetlenül fejlődnek; A logikának láthatónak, auditálhatónak vagy verziószámmal kell rendelkeznie; A nem mérnökök befolyásolják a viselkedést; Ugyanannak az űrlapnak több kezelőfelületen kell futnia.