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
return (
);}Podívejte se na Pen SurveyJS-03-RHF [rozvětvený] podle sixthextinction. Děje se toho tu docela dost a stojí za to zpomalit, abyste si všimli, kde co skončilo.
Odvozené hodnoty – mezisoučet, daň, celkem – se počítají v komponentě pomocí useWatch a useMemo, protože závisí na aktuálních hodnotách pole a neexistuje pro ně žádné jiné přirozené místo. Pravidla viditelnosti pro uživatelské jméno, heslo, positiveFeedback a ImproveFeedback fungují v JSX jako vložené podmínky. Logika přeskakování kroků – stránka recenze se zobrazí pouze v případě, že je celkový počet >= 100 – je vložena do proměnné showSubmit a do podmínky vykreslení v kroku 3. Samotná navigace je pouze počítadlo useState, které ručně zvyšujeme. React Query zpracovává opakování, ukládání do mezipaměti a zneplatnění. Formulář pouze volá mutation.mutate s ověřenými daty.
Nic z toho není samo o sobě špatné. Toto je stále idiomatický React a komponenta je docela výkonná díky tomu, jak RHF izoluje re-rendery. Ale pokud byste to měli předat někomu, kdo to nenapsal, a požádat ho, aby vysvětlil, za jakých podmínek se stránka s recenzí zobrazuje, musel by vysledovat pomocí showSubmit, podmínky vykreslování kroku 3 a logiku navigačního tlačítka – tři oddělená místa – aby rekonstruoval pravidlo, které mohlo být uvedeno na jednom řádku. Formulář funguje, ano, ale chování jako systém ve skutečnosti nelze zkontrolovat. Musí se to zvládnout psychicky. Ještě důležitější je, že jeho změna vyžaduje zapojení inženýrů. Dokonce i malé vylepšení, jako je úprava, když se zobrazí krok revize, znamená úpravu komponenty, aktualizaci ověření, otevření požadavku na stažení, čekání na kontrolu a opětovné nasazení. Část 2: Řízené schématem (SurveyJS) Nyní vytvoříme stejný tok pomocí schématu. Instalace npm install survey-core survey-react-ui @tanstack/react-query
survey-coreBěhový modul nezávislý na platformě licencovaný MIT, který pohání vykreslování formulářů SurveyJS – část, na které nám zde záleží. Vezme schéma JSON, sestaví z něj interní model a zvládá vše, co by jinak žilo ve vaší komponentě React: vyhodnocování výrazů viditelnosti, výpočet odvozených hodnot, správa stavu stránky, sledování ověřování a rozhodování o tom, co znamená „úplné“ vzhledem k tomu, které stránky se skutečně zobrazily.
survey-react-ui UI / renderovací vrstva, která spojuje tento model s React. Je to v podstatě komponenta
Dohromady vám poskytují plně funkční vícestránkový běhový modul formuláře bez psaní jediného řádku řídicího toku. Samotný formát schématu je, jak již bylo řečeno, pouze JSON – žádné DSL ani nic proprietárního. Můžete jej vložit, importovat ze souboru, načíst z API nebo uložit do databázového sloupce a hydratovat za běhu. Stejný formulář jako data Zde je stejný formulář, tentokrát vyjádřený jako objekt JSON. Schéma definuje vše: strukturu, validaci, pravidla viditelnosti, odvozené výpočty, navigaci po stránkách – a předává to modelu, který to vyhodnocuje za běhu. Takto to vypadá v plném znění:
export const surveySchema = { title: "Tok objednávky", showProgressBar: "top", pages: [ { name: "details", elements: [ { type: "text", name: "firstName", isRequired: true }, { type: "text", name: "email", inputType: "email", isRequired: true" "e-mail", "mail}]} ] }, { name: "order", elements: [ { type: "text", name: "price", inputType: "number", defaultValue: 0 }, { type: "text", name: "quantity", inputType: "number", defaultValue: 1 }, { type: "dropdown",name: "taxRate", defaultValue: 0.1, options: [ { value: 0.05, text: "5%" }, { value: 0.1, text: "10%" }, { value: 0.15, text: "15%" } ] }, { type: "expression", name: "{typeprice}", "", "typeprice}", výraz:} name: "daň", výraz: "{mezisoučet} {taxRate}" }, { typ: "výraz", název: "celkem", výraz: "{mezisoučet} + {daň" } ] }, { name: "account", elements: [ { type: "radiogroup", name: "hasAccount", možnosti: ["Ano", "Ne"] }, "hasc name", "No"] }, {type: "US" = text: 'Yes'", isRequired: true }, { type: "text", name: "password", inputType: "password", visibleIf: "{hasAccount} = 'Yes'", isRequired: true, validators: [{ type: "text", minLength: 6, text: "Min 6 characters" }] }, { name:1, rate: "name:1", rateMax: 5 }, { type: "comment", name: "positiveFeedback", visibleIf: "{satisfaction} >= 4" }, { type: "comment", name: "improvementFeedback", visibleIf: "{satisfaction} <= 2" } ] }, { name: "{satisfaction" 1 viditelná >=: "]:
Porovnejte to na chvíli s verzí RHF.
Blok superRefine, který podmíněně vyžadoval uživatelské jméno a heslo, je pryč. Viditelné If: "{hasAccount} = 'Ano'" v kombinaci s isRequired: true řeší oba problémy společně na poli samotném, kde byste je očekávali. Řetězec useWatch + useMemo, který vypočítal mezisoučet, daň a součet, je nahrazen třemi výrazovými poli, která na sebe odkazují jménem. Stav stránky recenze, který byl ve verzi RHF rekonstruovatelný pouze trasováním přes showSubmit, krok 3 render větve. A konečně, logika tlačítka nav je jediná vlastnost visibleIf na objektu stránky.
Je tam stejná logika. Jde jen o to, že schéma mu dává místo k životu, kde je vidět izolovaně, než aby se šířilo po komponentě. Všimněte si také, že schéma používá typ: 'výraz' pro mezisoučet, daň a součet. Výraz je pouze pro čtení a používá se hlavně k zobrazení vypočítaných hodnot. SurveyJS také podporuje typ: 'html' pro statický obsah, ale pro vypočítané hodnoty je výraz správnou volbou. Nyní ke straně React. Vykreslování A Odeslání Velmi jednoduché. Zapojte onComplete do svého API stejným způsobem – pomocí useMutation nebo prostého načtení:
import { useState, useEffect, useRef } z "react";import { useMutation } z "@tanstack/react-query";import { Model } z "survey-core";import { Survey } z "survey-react-ui";import "survey-core/survey-core.css";
export funkce SurveyForm() { const [model] = useState(() => new Model(surveySchema));
const mutation = useMutation({ mutationFn: async (data) => { const res = wait fetch("/api/orders", { metoda: "POST", záhlaví: { "Content-Type": "application/json" }, tělo: JSON.stringify(data), }); if (!res.ok) throw new Error("Nepodařilo se odeslat"); return res.json(); }, });
const mutationRef = useRef(mutace); mutationRef.current = mutace; useEffect(() => { const handler = (odesílatel) => mutationRef.current.mutate(odesílatel.data); model.onComplete.add(handler); return () => model.onComplete.remove(handler); }, [model]); // ref se vyhýbá opětovné registraci obslužného programu při každém vykreslení (změny identity objektu mutace)
vrátit (
<>
Viz Pen SurveyJS-03-SurveyJS [forked] by sixthextinction.
onComplete se spustí, když uživatel dosáhne konce poslední viditelné stránky. Pokud tedy součet nikdy nepřekročí 100 a stránka recenze je přeskočena, stále se spustí správně, protože SurveyJS vyhodnotí viditelnost, než se rozhodne, co znamená „poslední stránka“. Sender.data pak obsahuje všechny odpovědi spolu s vypočítanými hodnotami (mezisoučet, daň, celkem) jako prvotřídní pole, takže užitečné zatížení API je totožné s tím, co verze RHF sestavila ručně v onSubmit. TheVzor mutationRef je stejný, po kterém byste sáhli kdekoli, kde potřebujete stabilní obsluhu události nad hodnotou, která se mění při každém vykreslení – nic specifického pro SurveyJS.
Komponenta React již neobsahuje vůbec žádnou obchodní logiku. Nejsou zde žádné useWatch, žádné podmíněné JSX, žádné počítadlo kroků, žádný useMemo chain, žádné superRefine. React dělá to, v čem je vlastně dobrý: renderuje komponentu a zapojuje ji do volání API. Co se posunulo mimo React?
Obavy RHF Stack SurveyJS Viditelnost JSX větve viditelnýPokud Odvozené hodnoty useWatch / useMemo výraz Mezipolní pravidla superUpřesnit Podmínky schématu Navigace krokový stav Stránka viditelnáPokud Umístění pravidla Distribuováno mezi soubory Centralizováno ve schématu
To, co v Reactu zůstává, je rozvržení, styl, zapojení a integrace aplikací, což znamená, že React je ve skutečnosti navržen. Vše ostatní se přesunulo do schématu, a protože schéma je pouze objekt JSON, může být uloženo v databázi, verzováno nezávisle na kódu vaší aplikace nebo upravováno pomocí interních nástrojů bez nutnosti nasazení. Produktový manažer, který potřebuje změnit práh, který spouští stránku recenze, to může udělat, aniž by se komponenty dotkl. To je významný provozní rozdíl pro týmy, kde se chování formy často vyvíjí a není vždy řízeno inženýry. Kdy použít jednotlivé přístupy? Zde je dobré pravidlo, které pro mě funguje: představte si úplné smazání formuláře. co bys ztratil?
Pokud jde o obrazovky, chcete formuláře řízené komponentami. Pokud je to obchodní logika, jako jsou prahové hodnoty, pravidla větvení a podmíněné požadavky, které kódují skutečná rozhodnutí, chcete modul schémat.
Podobně, pokud se přicházející změny týkají především štítků, polí a rozvržení, RHF vám poslouží dobře. Pokud se týkají podmínek, výsledků a pravidel, které váš operační nebo právní tým možná bude muset upravit v úterý odpoledne bez vyplňování tiketu, je model schématu s SurveyJS poctivější. Tyto dva přístupy si ve skutečnosti navzájem nekonkurují. Zaměřují se na různé třídy problémů a chybou, které stojí za to se vyhnout, je nepřizpůsobení abstrakce k váze logiky – zacházet se systémem pravidel jako s komponentou, protože to je známý nástroj, nebo sáhnout po enginu politiky, protože formulář se rozrostl na tři kroky a získal podmíněné pole. Forma, kterou jsme zde vytvořili, záměrně sedí poblíž hranice, je dostatečně složitá, aby odhalila rozdíl, ale ne tak extrémní, aby se srovnání zdálo zmanipulované. Většina skutečných forem, které se ve vaší kódové základně staly nepraktickými, pravděpodobně leží poblíž stejné hranice a otázkou obvykle je, zda někdo pojmenoval, co vlastně jsou. Použijte React Hook Form + Zod, když:
Formuláře jsou orientované na CRUD; Logika je mělká a řízená uživatelským rozhraním; Inženýři vlastní veškeré chování; Backend zůstává zdrojem pravdy.
SurveyJS použijte, když:
Formuláře kódují obchodní rozhodnutí; Pravidla se vyvíjejí nezávisle na uživatelském rozhraní; Logika musí být viditelná, auditovatelná nebo verzovaná; Neinženýři ovlivňují chování; Stejný formulář musí běžet na více frontendech.