Tento článek je sponzorován SurveyJS Existuje mentální model, který většina vývojářů React sdílí, aniž by o něm kdy nahlas diskutovali. Že formuláře jsou vždy součástí. To znamená zásobník jako:

Formulář React Hook pro místní stát (minimální opětovné vykreslení, ergonomická registrace pole, nezbytná interakce). Zod pro ověření (správnost vstupu, ověření hranic, typově bezpečná analýza). React Query pro backend: odeslání, opakování, ukládání do mezipaměti, synchronizace serveru a tak dále.

A pro velkou většinu formulářů – vaše přihlašovací obrazovky, vaše stránky nastavení, vaše modály CRUD – to funguje opravdu dobře. Každý kus dělá svou práci, skládá se čistě a můžete přejít k částem vaší aplikace, které skutečně odlišují váš produkt. Ale jednou za čas začne formulář hromadit věci, jako jsou pravidla viditelnosti, která závisí na dřívějších odpovědích, nebo odvozené hodnoty, které kaskádovitě procházejí třemi poli. Možná dokonce celé stránky, které by měly být přeskočeny nebo zobrazeny na základě průběžného součtu. První podmínku zvládnete pomocí useWatch a inline větve, což je v pořádku. Pak další. Pak sáhnete po superRefine ke kódování pravidel napříč poli, která vaše schéma Zod nemůže vyjádřit normálním způsobem. Poté začne kroková navigace unikat obchodní logice. V určitém okamžiku se podíváte na to, co jste vytvořili, a uvědomíte si, že formulář už ve skutečnosti není uživatelské rozhraní. Je to spíše rozhodovací proces a strom komponent je právě tam, kde jste jej náhodou uložili. Tady si myslím, že se mentální model forem v Reactu hroutí a není to opravdu nikoho chyba. Stoh RHF + Zod je vynikající v tom, pro co byl navržen. Problém je v tom, že máme tendenci ho používat za bodem, kdy jeho abstrakce odpovídají problému, protože alternativa vyžaduje úplně jiný způsob uvažování o formách. Tento článek je o této alternativě. Abychom to ukázali, vytvoříme přesně stejný vícekrokový formulář dvakrát:

S React Hook Form + Zod připojeným k React Query k odeslání, S SurveyJS, který zachází s formulářem jako s daty – jednoduchým schématem JSON – spíše než se stromem komponent.

Stejné požadavky, stejná podmíněná logika, stejné volání API na konci. Poté přesně zmapujeme, co se přesunulo a co zůstalo, a navrhneme praktický způsob, jak se rozhodnout, který model byste měli použít a kdy. Formulář, který vytváříme:

Tento formulář bude používat 4krokový postup: Krok 1: Podrobnosti

Křestní jméno (povinné), E-mail (povinné, platný formát).

Krok 2: Objednávka

jednotková cena, množství, daňová sazba, odvozeno: Mezisoučet, daň, Celkem.

Krok 3: Účet a zpětná vazba

Máte účet? (Ano/Ne) Pokud ano → uživatelské jméno + heslo, obojí je povinné. Pokud Ne → e-mail již byl shromážděn v kroku 1.

Hodnocení spokojenosti (1–5) Pokud ≥ 4 → zeptejte se „Co se vám líbilo?“ Pokud ≤ 2 → zeptejte se „Co můžeme zlepšit?“

Krok 4: Kontrola

Zobrazí se pouze v případě, že celkový počet >= 100 Konečné podání.

To není extrém. Ale na odhalení architektonických rozdílů to stačí. Část 1: Součásti řízené (React Hook Form + Zod) Instalace npm install response-hook-form zod @hookform/resolvers @tanstack/react-query

Schéma Zod Začněme schématem Zod, protože tam se obvykle utváří tvar formuláře. V prvních dvou krocích – osobní údaje a zadání objednávky – je vše jednoduché: požadované řetězce, čísla s minimy a výčet. Zajímavá část začíná, když se pokusíte vyjádřit podmíněná pravidla.

import { z } z "zod";

export const formSchema = z.object({ jméno: z.string().min(1, "Povinné"), e-mail: z.string().email("Neplatný e-mail"), cena: z.číslo().min(0), množství: z.číslo().min(1), daňová sazba: z.číslo(), hasAccount: z.číslo_uživatele:z.číslo_uživatelského_čísla:z.číslo_uživatelské_číslo:z.string_nepovinné,heslo“] z.string().nepovinné(), spokojenost: z.číslo().min(1).max(5), pozitivní zpětná vazba: z.string().nepovinné(), zlepšeníZpětná vazba: z.string().nepovinné(),}).superRefine((data, ctx) => { if (data.hasAccount === "Ano") { if (!custom{100}{101}data_username" cesta: ["uživatelské jméno"], zpráva: "Povinné" } } if (!data.password || data.password.length < 6) { ctx.addIssue({ kód: "vlastní", cesta: ["heslo"], zpráva: "Min 6 znaků" } });

if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ kód: "custom", cesta: ["positiveFeedback"], zpráva: "Sdílejte, co se vám líbilo" }); }

if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ kód: "vlastní", cesta:["improvementFeedback"], zpráva: "Řekněte nám, co máme zlepšit" }); }});

typ exportu FormData = z.infer;

Všimněte si, že uživatelské jméno a heslo se zadávají jako volitelné(), i když jsou podmíněně vyžadovány, protože Zodovo schéma na úrovni typu popisuje tvar objektu, nikoli pravidla, kterými se řídí, když na polích záleží. Podmíněný požadavek musí existovat uvnitř superRefine, který běží po ověření tvaru a má přístup k celému objektu. Toto oddělení není vada; je to přesně to, k čemu je nástroj navržen: superRefine je místo, kde jde logika napříč poli, když ji nelze vyjádřit ve struktuře schématu samotné. Co je zde také pozoruhodné, je to, co toto schéma nevyjadřuje. Nemá žádnou koncepci stránek, žádnou koncepci, která pole jsou v kterém místě viditelná, a žádnou koncepci navigace. To vše bude žít někde jinde. Komponenta formuláře

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", "recenze"];

zadejte OrderPayload = FormData & { mezisoučet: číslo; daň: číslo; celkem: počet };

exportní funkce RHFMultiStepForm() { const [krok, setStep] = useState(0);

const mutation = useMutation({ mutationFn: async (užitná zátěž: OrderPayload) => { const res = wait fetch("/api/orders", { metoda: "POST", záhlaví: { "Content-Type": "application/json" }, tělo: JSON.stringify(užitná zátěž), }); if (!res.ok) throw new Error("Nepodařilo se odeslat"); return res.json(); }, });

const { register, control, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(formSchema), defaultValues: { cena: 0, množství: 1, taxRate: 0,1, spokojenost: 3, hasAccount: "No", }, }); const price = useWatch({ control, name: "price" }); const mnozstvi = useWatch({ control, name: "quantity" }); const taxRate = useWatch({ control, name: "taxRate" }); const hasAccount = useWatch({ control, name: "hasAccount" }); const spokojenost = useWatch({ control, name: "satisfaction" }); const mezisoučet = useMemo(() => (cena ?? 0) * (množství ?? 1), [cena, množství]); const tax = useMemo(() => mezisoučet * (taxRate ?? 0), [mezisoučet, taxRate]); const total = useMemo(() => mezisoučet + daň, [mezisoučet, daň]); const onSubmit = (data: FormData) => mutation.mutate({ ...data, mezisoučet, daň, celkem }); const showSubmit = (krok === 2 && celkem < 100) || (krok === 3 && celkem >= 100)

return (

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

{step === 1 && ( <>

Mezisoučet: {subtotal}
Daň: {tax}
Celkem: {total}
)}

{step === 2 && ( <>

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

{satisfaction >= 4 && (