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({ feloldó: zodResolver(formSchema), defaultValues: { ár: 0, mennyiség: 1, taxRate: 0.1, elégedettség: 3, hasAccount: "No", }, }); const price = useWatch({ vezérlő, név: "ár" }); const mennyiség = useWatch({ vezérlőelem, név: "mennyiség" }); const taxRate = useWatch({ vezérlő, név: "taxRate" }); const hasAccount = useWatch({ vezérlőelem, név: "hasAccount" }); const satisfaction = useWatch({ vezérlőelem, név: "elégedettség" }); const részösszeg = useMemo(() => (ár ?? 0) * (mennyiség ?? 1), [ár, mennyiség]); const tax = useMemo(() => részösszeg * (taxRate ?? 0), [részösszeg, taxRate]); const total = useMemo(() => részösszeg + adó, [részösszeg, adó]); const onSubmit = (adatok: FormData) => mutation.mutate({ ...adat, részösszeg, adó, összesen }); const showSubmit = (lépés === 2 && összesen < 100) || (lépés === 3 && összesen >= 100)

return (

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

{step === 1 && ( <>

Részösszeg: {subtotal}
Adó: {tax}
Összesen: {total}
)}

{step === 2 && ( <>

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

{elégedettség >= 4 && (