Ovaj članak sponzorira SurveyJS Postoji mentalni model koji većina React programera dijeli, a da o tome nikada ne razgovaraju naglas. Te forme bi uvijek trebale biti komponente. To znači stog poput:

React Hook Form za lokalno stanje (minimalni ponovni prikazi, registracija ergonomskog polja, imperativna interakcija). Zod za provjeru valjanosti (ispravnost unosa, provjera granice, raščlanjivanje bezbjednog tipa). React Query za backend: podnošenje, ponovni pokušaji, keširanje, sinhronizacija servera, itd.

A za ogromnu većinu obrazaca – vaše ekrane za prijavu, vaše stranice s postavkama, vaše CRUD modale – ovo funkcionira jako dobro. Svaki komad radi svoj posao, komponuje se čisto, a možete prijeći na dijelove vaše aplikacije koji zapravo razlikuju vaš proizvod. Ali s vremena na vrijeme, obrazac počinje akumulirati stvari kao što su pravila vidljivosti koja zavise od ranijih odgovora ili izvedene vrijednosti koje kaskadiraju kroz tri polja. Možda čak i čitave stranice koje bi trebalo preskočiti ili prikazati na osnovu tekućeg zbroja. S prvim uslovom rukujete pomoću useWatch-a i inline grane, što je u redu. Onda još jedan. Zatim posežete za superRefine da kodira pravila unakrsnih polja koja vaša Zod shema ne može izraziti na normalan način. Zatim, navigacija koraka počinje da curi poslovnu logiku. U nekom trenutku, pogledate šta ste napravili i shvatite da forma više nije UI. To je više proces odlučivanja, a stablo komponenti je upravo tamo gdje ste ga pohranili. Ovdje mislim da se mentalni model za forme u React-u lomi, i zapravo niko nije kriv. RHF + Zod stack je odličan u onome za šta je dizajniran. Problem je u tome što smo skloni da ga koristimo nakon tačke u kojoj se njene apstrakcije podudaraju s problemom jer alternativa zahtijeva potpuno drugačiji način razmišljanja o oblicima. Ovaj članak je o toj alternativi. Da bismo to pokazali, dvaput ćemo napraviti potpuno isti obrazac u više koraka:

Sa React Hook Form + Zod povezanim na React Query za podnošenje, Sa SurveyJS, koji tretira formu kao podatke — jednostavnu JSON šemu — umjesto kao stablo komponenti.

Isti zahtjevi, ista uvjetna logika, isti API poziv na kraju. Zatim ćemo mapirati tačno šta se pomerilo, a šta je ostalo, i izložiti praktičan način da odlučite koji model treba da koristite i kada. Forma koju gradimo:

Ovaj obrazac će koristiti tok u 4 koraka: Korak 1: Detalji

Ime (obavezno), E-pošta (obavezno, važeći format).

Korak 2: Naručite

jedinična cijena, količina, poreska stopa, Izvedeno: Međuzbroj, porez, Ukupno.

Korak 3: Račun i povratne informacije

Imate li račun? (da/ne) Ako Da → korisničko ime + lozinka, oba su potrebna. Ako Ne → e-pošta je već prikupljena u koraku 1.

Ocjena zadovoljstva (1–5) Ako ≥ 4 → pitajte “Šta vam se svidjelo?” Ako je ≤ 2 → pitajte "Šta možemo poboljšati?"

Korak 4: Pregledajte

Pojavljuje se samo ako je ukupno >= 100 Konačna predaja.

Ovo nije ekstremno. Ali dovoljno je da se razotkriju arhitektonske razlike. Dio 1: Komponenta vođena (React Hook Form + Zod) Instalacija npm install react-hook-form zod @hookform/resolvers @tanstack/react-query

Zod Schema Počnimo sa Zod šemom, jer se tu obično uspostavlja oblik forme. Za prva dva koraka — lične podatke i unos narudžbe — sve je jednostavno: potrebni nizovi, brojevi sa minimumom i enum. Zanimljivi dio počinje kada pokušate izraziti uvjetna pravila.

import { z } iz "zod";

export const formSchema = z.object({ firstName: z.string().min(1, "Obavezno"), email: z.string().email("Invalid email"), cijena: z.number().min(0), količina: z.number().min(1), taxRate: z.number(), hasAccount: z. z.string().optional(), lozinka: z.string().optional(), zadovoljstvo: z.number().min(1).max(5), pozitivnaFeedback: z.string().optional(), poboljšanjeFeedback: z.string().optional(),}).superRefine((data, ctxta) => {s if (Ycdacount) => {s if (Ycdacount) (!data.username) { ctx.addIssue({ code: "custom", path: ["username"], message: "required" } } if (!data.password || data.password.length < 6) { ctx.addIssue({ code: "password", path:]); }

if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ code: "custom", path: ["positiveFeedback"], poruka: "Podijelite ono što vam se sviđa" }); }

if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ code: "custom", put:["improvementFeedback"], poruka: "Molim vas, recite nam šta da poboljšamo" }); }});

tip izvoza FormData = z.infer;

Primijetite da se korisničko ime i lozinka upisuju kao opcioni() iako su uvjetno obavezni jer Zodova shema na razini tipa opisuje oblik objekta, a ne pravila koja određuju kada su polja bitna. Uslovni zahtjev mora živjeti unutar superRefine, koji se pokreće nakon što je oblik potvrđen i ima pristup cijelom objektu. To razdvajanje nije mana; to je upravo ono za šta je alat dizajniran: superRefine je mjesto gdje ide logika unakrsnih polja kada se ne može izraziti u samoj strukturi šeme. Ono što je takođe primetno ovde je ono što ova šema ne izražava. Nema koncept stranica, nema koncepta koja polja su vidljiva u kojoj tački, niti koncept navigacije. Sve će to živjeti negdje drugdje. Komponenta obrasca

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

const STEPS = ["detalji", "narudžba", "račun", "pregled"];

type OrderPayload = FormData & { subtotal: broj; porez: broj; ukupno: broj };

eksport funkcija RHFMultiStepForm() { const [korak, setStep] = useState(0);

const mutation = useMutation({ mutationFn: async (korisno opterećenje: OrderPayload) => { const res = čekaj dohvat("/api/orders", { metoda: "POST", zaglavlja: { "Content-Type": "application/json" }, tijelo: JSON.stringify (korisno opterećenje), }); if (!res.ok) throw new Error("Neuspjelo slanje"); return res.json(); }, });

const { register, control, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(formSchema), defaultValues: { cijena: 0, količina: 1, stopa poreza: 0,1, zadovoljstvo: 3, ima račun, "Ne", ); const cijena = useWatch({ kontrola, naziv: "price" }); const quantity = useWatch({ kontrola, naziv: "količina" }); const taxRate = useWatch({ kontrola, ime: "taxRate" }); const hasAccount = useWatch({ kontrola, ime: "hasAccount" }); const satisfaction = useWatch({ kontrola, ime: "satisfaction" }); const subtotal = useMemo(() => (cijena ?? 0) * (količina ?? 1), [cijena, količina]); const porez = useMemo(() => međuzbroj * (taxRate ?? 0), [subtotal, taxRate]); const total = useMemo(() => međuzbroj + porez, [podzbroj, porez]); const onSubmit = (podaci: FormData) => mutation.mutate({ ...podaci, međuzbroj, porez, ukupno }); const showSubmit = (korak === 2 && ukupno < 100) || (korak === 3 && ukupno >= 100)

return (

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

{step === 1 && ( <>

Subtotal: {subtotal}
Porez: {tax}
Ukupno: {total}
)}

{step === 2 && ( <>

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

{satisfaction >= 4 && (