Ovaj članak sponzorira SurveyJS Postoji mentalni model koji većina React programera dijeli, a da o njemu nikada ne raspravljaju naglas. Da forme uvijek trebaju biti komponente. To znači hrpu poput:

Obrazac React Hook za lokalno stanje (minimalno ponovno renderiranje, ergonomska registracija polja, imperativna interakcija). Zod za provjeru valjanosti (ispravnost unosa, provjera valjanosti granica, sigurnosno raščlanjivanje). React Query za backend: podnošenje, ponovni pokušaji, predmemorija, sinkronizacija poslužitelja itd.

A za veliku većinu obrazaca - vaše zaslone za prijavu, vaše stranice postavki, vaše CRUD modale - ovo jako dobro funkcionira. Svaki komad radi svoj posao, čisto se komponiraju i možete prijeći na dijelove svoje aplikacije koji zapravo razlikuju vaš proizvod. No s vremena na vrijeme obrazac počne gomilati stvari poput pravila vidljivosti koja ovise o ranijim odgovorima ili izvedenih vrijednosti koje se nižu kroz tri polja. Možda čak i cijele stranice koje bi se trebale preskočiti ili prikazati na temelju tekućeg ukupnog broja. Prvim uvjetom upravljate s useWatčem i inline granom, što je u redu. Zatim još jedan. Zatim posežete za superRefineom za kodiranje pravila više polja koja vaša Zod shema ne može izraziti na normalan način. Zatim, postupna navigacija počinje gubiti poslovnu logiku. U nekom trenutku pogledate što ste izgradili i shvatite da forma zapravo više nije korisničko sučelje. To je više proces odlučivanja, a stablo komponenti je upravo mjesto gdje ste ga pohranili. Ovdje mislim da se mentalni model za forme u Reactu kvari, a to zapravo nije ničija krivnja. RHF + Zod skup izvrstan je u onome za što je dizajniran. Problem je u tome što smo skloni nastaviti ga koristiti nakon točke u kojoj se njegove apstrakcije poklapaju s problemom jer alternativa zahtijeva sasvim drugačiji način razmišljanja o oblicima. Ovaj članak govori o toj alternativi. Da bismo to pokazali, dvaput ćemo izraditi točno isti obrazac u više koraka:

Uz React Hook obrazac + Zod spojen na React Query za slanje, Uz SurveyJS, koji obrazac tretira kao podatke — jednostavnu JSON shemu — umjesto stabla komponenti.

Isti zahtjevi, ista uvjetna logika, isti API poziv na kraju. Zatim ćemo mapirati točno što se pomaknulo, a što je ostalo i iznijeti praktičan način da odlučite koji model trebate koristiti i kada. Obrazac koji gradimo:

Ovaj će obrazac koristiti tijek od 4 koraka: 1. korak: pojedinosti

Ime (obavezno), Email (obavezno, važeći format).

Korak 2: Naručite

Jedinična cijena, količina, porezna stopa, Izvedeno: Međuzbroj, porez, Ukupno.

Korak 3: Račun i povratne informacije

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

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

Korak 4: Pregled

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

Ovo nije ekstremno. Ali to je dovoljno za izlaganje arhitektonskih razlika. 1. dio: Pokretan komponentom (React Hook Form + Zod) Instalacija npm instaliraj react-hook-form zod @hookform/resolvers @tanstack/react-query

Zod shema Počnimo sa Zod shemom, jer se tamo obično uspostavlja oblik forme. Za prva dva koraka — osobne podatke i unos narudžbe — sve je jednostavno: potrebni nizovi, brojevi s minimumima i enum. Zanimljiv dio počinje kada pokušate izraziti uvjetna pravila.

import { z } iz "zod";

export const formSchema = z.object({ firstName: z.string().min(1, "Required"), email: z.string().email("Invalid email"), price: z.number().min(0), quantity: z.number().min(1), taxRate: z.number(), hasAccount: z.enum(["Yes", "No"]), username: z.string().optional(), lozinka: z.string().optional(), zadovoljstvo: z.number().min(1).max(5), positiveFeedback: z.string().optional(), poboljšanjaFeedback: z.string().optional(),}).superRefine((data, ctx) => { if (data.hasAccount === "Da") { if (!data.username) { ctx.addIssue({ kod: "prilagođeno", put: ["korisničko ime"], poruka: "Potrebno" });

if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ code: "custom", path: ["positiveFeedback"], message: "Molimo podijelite što vam se svidjelo" }); }

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

tip izvoza FormData = z.infer;

Primijetite da su korisničko ime i lozinka upisani kao optional() iako su uvjetno obavezni jer Zod-ova shema na razini tipa opisuje oblik objekta, a ne pravila koja određuju kada su polja bitna. Uvjetni zahtjev mora živjeti unutar superRefine, koji se pokreće nakon što se oblik potvrdi i ima pristup cijelom objektu. Ta odvojenost nije mana; to je upravo ono za što je alat dizajniran: superRefine je mjesto gdje ide logika više polja kada se ne može izraziti u samoj strukturi sheme. Ono što je također važno ovdje je ono što ova shema ne izražava. Nema pojma stranica, pojma koja su polja vidljiva na kojem mjestu, niti koncepta 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, type FormData } from "./schema";

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

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

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

const mutacija = useMutation({ mutationFn: async (payload: OrderPayload) => { const res = await fetch("/api/orders", { metoda: "POST", zaglavlja: { "Content-Type": "application/json" }, tijelo: JSON.stringify(payload), }); if (!res.ok) throw new Error("Failed to submit"); vrati res.json(); }, });

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

return (

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

{korak === 1 && ( <> 5%

Međuzbroj: {subtotal}
Porez: {tax}
Ukupno: {total}
)}

{korak === 2 && ( <>

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

{zadovoljstvo >= 4 && (