Tento článok je sponzorovaný spoločnosťou SurveyJS Existuje mentálny model, ktorý väčšina vývojárov Reactu zdieľa bez toho, aby o tom nahlas diskutovali. Že formuláre sú vždy súčasťou. To znamená zásobník ako:

Formulár React Hook pre miestny štát (minimálne opätovné vykreslenie, ergonomická registrácia v teréne, nevyhnutná interakcia). Zod na overenie (správnosť vstupu, overenie hraníc, typovo bezpečná analýza). React Query pre backend: odoslanie, opakované pokusy, ukladanie do vyrovnávacej pamäte, synchronizácia servera atď.

A pre veľkú väčšinu formulárov – vaše prihlasovacie obrazovky, stránky s nastaveniami, vaše modály CRUD – to funguje naozaj dobre. Každý kus robí svoju prácu, skladá sa čisto a môžete prejsť k častiam vašej aplikácie, ktoré skutočne odlišujú váš produkt. Raz za čas však formulár začne hromadiť veci, ako sú pravidlá viditeľnosti, ktoré závisia od skorších odpovedí, alebo odvodené hodnoty, ktoré kaskádovito prechádzajú cez tri polia. Možno aj celé stránky, ktoré by sa mali preskočiť alebo zobraziť na základe priebežného súčtu. Prvú podmienku zvládnete pomocou useWatch a inline vetvy, čo je v poriadku. Potom ďalší. Potom siahate po superRefine, aby ste zakódovali pravidlá medzi poľami, ktoré vaša schéma Zod nedokáže vyjadriť normálnym spôsobom. Potom začne kroková navigácia presakovať obchodnú logiku. V určitom okamihu sa pozriete na to, čo ste vytvorili, a uvedomíte si, že formulár už v skutočnosti nie je používateľským rozhraním. Je to skôr rozhodovací proces a strom komponentov je presne tam, kde ste ho náhodou uložili. Tu si myslím, že mentálny model pre formy v Reacte sa rozpadá a v skutočnosti za to nemôže nikto. Stoh RHF + Zod je vynikajúci v tom, na čo bol navrhnutý. Problém je v tom, že máme tendenciu ho používať až za bod, kde jeho abstrakcie zodpovedajú problému, pretože alternatíva vyžaduje úplne iný spôsob myslenia o formách. Tento článok je o tejto alternatíve. Aby sme to ukázali, vytvoríme presne ten istý viackrokový formulár dvakrát:

S React Hook Form + Zod pripojeným k React Query na odoslanie, S SurveyJS, ktorý zaobchádza s formulárom ako s údajmi – jednoduchou schémou JSON – a nie so stromom komponentov.

Rovnaké požiadavky, rovnaká podmienená logika, rovnaké volanie API na konci. Potom presne zmapujeme, čo sa presťahovalo a čo zostalo, a navrhneme praktický spôsob, ako rozhodnúť, ktorý model by ste mali použiť a kedy. Formulár, ktorý vytvárame:

Tento formulár bude používať 4-krokový postup: Krok 1: Podrobnosti

Krstné meno (povinné), E-mail (povinné, platný formát).

Krok 2: Objednávka

jednotková cena, množstvo, sadzba dane, odvodené: medzisúčet, daň, Celkom.

Krok 3: Účet a spätná väzba

Máte účet? (Áno/Nie) Ak Áno → používateľské meno + heslo, musia sa obidva údaje. Ak Nie → e-mail už bol zhromaždený v kroku 1.

Hodnotenie spokojnosti (1 – 5) Ak ≥ 4 → opýtajte sa „Čo sa vám páčilo?“ Ak ≤ 2 → opýtajte sa „Čo môžeme zlepšiť?“

Krok 4: Kontrola

Zobrazí sa iba vtedy, ak je celkový počet >= 100 Záverečné podanie.

To nie je extrém. Ale na odhalenie architektonických rozdielov to stačí. Časť 1: Poháňané komponentmi (React Hook Form + Zod) Inštalácia npm install response-hook-form zod @hookform/resolvers @tanstack/react-query

Schéma Zod Začnime so schémou Zod, pretože to je zvyčajne miesto, kde sa vytvorí tvar formulára. V prvých dvoch krokoch – osobné údaje a zadanie objednávky – je všetko jednoduché: požadované reťazce, čísla s minimami a enum. Zaujímavá časť začína, keď sa pokúsite vyjadriť podmienené pravidlá.

import { z } z "zod";

export const formSchema = z.object({ meno: z.string().min(1, "Povinné"), e-mail: z.string().email("Neplatný e-mail"), cena: z.číslo().min(0), množstvo: z.číslo().min(1), sadzba dane: z.číslo(), hasAccount: z.enum: z.meno používateľa(["] voliteľné), z.string). z.string().voliteľné(), spokojnosť: z.číslo().min(1).max(5), pozitívna spätná väzba: z.string().voliteľné(), zlepšenieSpätná väzba: z.string().voliteľné(),}).superUpresniť((údaje, ctx) => { if (data.hasAccount === "Áno") { ife (!custom{data.dx:meno používateľa. cesta: ["username"], správa: "Povinné" } } if (!data.password || data.password.length < 6) { ctx.addIssue({ kód: "vlastné", cesta: ["heslo"], správa: "Minimálne 6 znakov" } });

if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ kód: "custom", cesta: ["positiveFeedback"], správa: "Zdieľajte, čo sa vám páčilo" }); }

if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ kód: "vlastné", cesta:["improvementFeedback"], správa: "Povedzte nám, čo máme zlepšiť" }); }});

typ exportu FormData = z.infer;

Všimnite si, že používateľské meno a heslo sú zadané ako voliteľné (), aj keď sú podmienene povinné, pretože Zodova schéma na úrovni typu popisuje tvar objektu, nie pravidlá, ktorými sa riadi, keď na poliach záleží. Podmienená požiadavka musí existovať vo vnútri superRefine, ktorá sa spustí po overení tvaru a má prístup k celému objektu. Toto oddelenie nie je chybou; je to presne to, na čo je tento nástroj určený: superRefine je miesto, kde ide logika medzi poľami, keď ju nemožno vyjadriť v samotnej štruktúre schémy. Pozoruhodné je aj to, čo táto schéma nevyjadruje. Nemá žiadnu koncepciu stránok, žiadnu koncepciu toho, ktoré polia sú v ktorom bode viditeľné, a žiadnu koncepciu navigácie. To všetko bude žiť niekde inde. Komponent formulára

import { useForm, useWatch } z "react-hook-form";import { zodResolver } z "@hookform/resolvers/zod";import { useMutation } z "@tanstack/react-query";import { useState, useMemo } z "react";import" {formDatachema};

const STEPS = ["podrobnosti", "objednávka", "účet", "recenzia"];

zadajte OrderPayload = FormData & { medzisúčet: číslo; daň: číslo; celkove: cislo };

exportova funkcia RHFMultiStepForm() { const [krok, setStep] = useState(0);

const mutácia = useMutation({ mutationFn: async (užitočné zaťaženie: OrderPayload) => { const res = wait fetch("/api/orders", { metóda: "POST", hlavičky: { "Content-Type": "application/json" }, telo: JSON.stringify(úžitkové zaťaženie), }); if (!res.ok) hodiť novú chybu("Nepodarilo sa odoslať"); return res.json(); }, });

const { register, control, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(formSchema), defaultValues: { cena: 0, mnozstvo: 1, taxRate: 0.1, spokojnost: 3, hasAccount: "No", }, }); const price = useWatch({ control, name: "price" }); const mnozstvo = useWatch({ control, name: "quantity" }); const taxRate = useWatch({ control, name: "taxRate" }); const hasAccount = useWatch({ control, name: "hasAccount" }); const spokojnost = useWatch({ kontrola, meno: "spokojnost" }); const medzisúčet = useMemo(() => (cena ?? 0) * (množstvo ?? 1), [cena, množstvo]); const tax = useMemo(() => medzisúčet * (daňová sadzba ?? 0), [medzisúčet, daňová sadzba]); const total = useMemo(() => medzisúčet + daň, [medzisúčet, daň]); const onSubmit = (údaje: FormData) => mutation.mutate({ ...údaje, medzisúčet, daň, súčet }); const showSubmit = (krok === 2 && spolu < 100) || (krok === 3 && spolu >= 100)

return (

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

{step === 1 && ( <>

Medzisúčet: {subtotal}
Daň: {tax}
Celkom: {total}
)}

{step === 2 && ( <>

{hasAccount === "Áno" && ( <> )}

{satisfaction >= 4 && (