# Što ćete naučiti#
Ovaj vodič pokriva najbolje prakse za React obrasce u velikim proizvodima, gdje obrasci mogu imati stotine polja, protezati se kroz više koraka, integrirati se s API-jima te zahtijevati pouzdanu validaciju i pristupačnost. Naučit ćete obrasce koji održavaju kod obrazaca održivim, brzim i dosljednim kroz timove.
Fokusirat ćemo se na React Hook Form za upravljanje stanjem i Zod za validaciju sheme, uz primjere koji dobro funkcioniraju u Next.js i modernom Reactu.
# Zašto se obrasci raspadaju u velikom mjerilu#
U malim aplikacijama uobičajeno je držati validaciju inline, razbacati props-e inputa po komponentama i rješavati greške ad hoc. U velikom mjerilu to stvara mjerljive probleme:
- Nedosljedna pravila validacije između klijenta i servera dovode do različitog ponašanja i support ticketa.
- Regresije performansi pojavljuju se kao “lag” pri tipkanju kada slučajno re-renderirate cijeli obrazac pri svakoj promjeni inputa.
- Rupe u pristupačnosti povećavaju pravni i compliance rizik te smanjuju konverzije za korisnike tipkovnice i čitača ekrana.
- Usporava se razvoj novih značajki jer nova polja zahtijevaju copy-paste obrazaca umjesto slaganja kompozabilnih gradivnih blokova.
Rješenje je arhitektura obrazaca koja je schema-first, komponentizirana i predvidljiva.
# Pregled arhitekture: Schema-first, komponentizirano, validirano na serveru#
Skalabilan pristup tipično izgleda ovako:
| Sloj | Odgovornost | Preporučeni obrazac |
|---|---|---|
| Zod schema | Izvor istine za oblik i pravila | Jedna shema po obrascu i opcionalno po koraku |
| React Hook Form | Stanje, praćenje izmjena (dirty), slanje, greške | useForm + FormProvider + useFormContext |
| Field komponente | Dosljedan UI, labeli, greške, aria povezivanje | wrapperi TextField, SelectField, CheckboxField |
| Async validacija | Provjera jedinstvenosti, udaljena pravila | Debounceani API poziv + setError |
| Server validacija | Konačni autoritet, sigurnost | Validiraj payload istom Zod shemom na serveru |
| Mapiranje grešaka | Pretvaranje API grešaka u RHF greške | Tipizirani error contract i funkcija mapiranja |
Ako vaš proizvod treba i skalabilnu UI osnovu, uskladite field komponente s vašim design systemom. Za strukturu komponenti i granice vlasništva pogledajte React arhitekturu komponenti za skalabilne design sisteme.
# Korak 1: Definirajte Zod sheme koje skaliraju#
Zod shema treba biti laka za čitanje, ponovnu upotrebu i testiranje. Najveći dobitak u skalabilnosti je izrada domenskih shema koje se mogu kompozicijom pretvoriti u sheme obrazaca.
Primjer: kompozicija domenskih shema#
import { z } from "zod";
const Email = z.string().email("Enter a valid email").max(254);
const Phone = z
.string()
.regex(/^\+?[0-9\s-]{7,20}$/, "Enter a valid phone number")
.optional();
export const CustomerSchema = z.object({
firstName: z.string().min(1, "First name is required").max(80),
lastName: z.string().min(1, "Last name is required").max(80),
email: Email,
phone: Phone,
});
export type CustomerInput = z.infer<typeof CustomerSchema>;Ovo je važno jer shema postaje zajednički ugovor za:
- validaciju na klijentu
- validaciju na serveru
- API dokumentaciju i tipizaciju
- testne fixture-e
Validacija između polja uz superRefine#
Složeni proizvodi često trebaju validacije poput “datum završetka mora biti nakon datuma početka” ili “ako je postavljena tvrtka, PDV ID je obavezan”.
export const BillingSchema = z
.object({
isCompany: z.boolean(),
companyName: z.string().optional(),
vatId: z.string().optional(),
})
.superRefine((data, ctx) => {
if (data.isCompany) {
if (!data.companyName) {
ctx.addIssue({
code: "custom",
path: ["companyName"],
message: "Company name is required",
});
}
if (!data.vatId) {
ctx.addIssue({
code: "custom",
path: ["vatId"],
message: "VAT ID is required",
});
}
}
});⚠️ Upozorenje: Nemojte se oslanjati na validaciju samo na klijentu za kritična pravila. Uvijek validirajte ponovno na serveru kako biste spriječili zaobilaženja i probleme s integritetom podataka. Koristite sigurnosnu osnovu poput Web Application Security Checklist kako biste izbjegli česte zamke.
# Korak 2: Povežite React Hook Form i Zod na ispravan način#
React Hook Form je brz prvenstveno zato što koristi uncontrolled inpute i pretplate (subscriptions) kako bi izbjegao globalne re-render-e. Zadržite tu prednost tako da izbjegavate obrasce koji čitaju cijelo stanje obrasca pri svakom pritisku tipke.
Osnovno postavljanje s resolverom i tipiziranim default vrijednostima#
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { CustomerSchema, type CustomerInput } from "./schemas";
const defaultValues: CustomerInput = {
firstName: "",
lastName: "",
email: "",
phone: undefined,
};
export function useCustomerForm() {
return useForm<CustomerInput>({
defaultValues,
resolver: zodResolver(CustomerSchema),
mode: "onSubmit",
reValidateMode: "onChange",
shouldFocusError: true,
});
}Preporučene postavke za velike obrasce:
mode: "onSubmit"sprječava bučne rane greške na velikim formama.reValidateMode: "onChange"daje brzi feedback nakon prvog submit-a.shouldFocusError: truepomaže korisnicima tipkovnice i smanjuje trenje.
💡 Savjet: Kad UX zahtijeva trenutnu validaciju, za “teža” pravila radije koristite
onBlurnegoonChange. Smanjuje “validation spam” i poboljšava dojam performansi kod dugih obrazaca.
# Korak 3: Izgradite višekratno upotrebljive field komponente koje se ne bore s RHF-om#
U velikom mjerilu, najveći dobitak u održavanju su standardni wrapperi polja koji dosljedno rješavaju label, pomoćni tekst, greške i aria atribute.
Kontrakt field komponente#
Dobar wrapper polja treba:
- prihvatiti
namekao tipizirani path - renderirati label i povezati ga s inputom
- renderirati poruku greške s
role="alert" - postaviti
aria-invalidiaria-describedby - raditi i s
registeri sControllerslučajevima
Primjer: TextField s useFormContext#
import { useId } from "react";
import { useFormContext } from "react-hook-form";
type TextFieldProps = {
name: "firstName" | "lastName" | "email" | "phone";
label: string;
type?: "text" | "email" | "tel";
placeholder?: string;
};
export function TextField(props: TextFieldProps) {
const { name, label, type = "text", placeholder } = props;
const { register, formState } = useFormContext();
const error = formState.errors[name]?.message?.toString();
const inputId = useId();
const errorId = `${inputId}-error`;
return (
<>
**{label}**
<br />
<input
id={inputId}
type={type}
placeholder={placeholder}
aria-invalid={error ? "true" : "false"}
aria-describedby={error ? errorId : undefined}
{...register(name)}
/>
{error ? (
<>
<br />
<span id={errorId} role="alert">
{error}
</span>
</>
) : null}
</>
);
}Ključna poanta: wrapper čita samo grešku za jedno polje. U produkciji biste osnovni markup zamijenili komponentama iz design systema, ali biste zadržali isto ponašanje i props-e.
Kada koristiti Controller#
Koristite Controller za controlled komponente poput custom selectova, date pickera, maskiranih inputa ili rich text editora. Nemojte sve forsirati kroz Controller, jer može povećati broj renderiranja.
# Korak 4: Async validacijski obrasci koji ne stvaraju UX “lag”#
Asinkrone provjere su česte u složenim proizvodima:
- jedinstvenost emaila
- validacija kupona
- normalizacija adrese
- provjera VAT ID-a
- provjera dostupnosti korisničkog imena
Napravite to tako da izbjegnete spam prema serveru i da greške ostanu stabilne.
Obrazac: debounceani async validator s useWatch#
import { useEffect, useMemo } from "react";
import { useFormContext, useWatch } from "react-hook-form";
function debounce(fn: () => void, ms: number) {
let t: ReturnType<typeof setTimeout> | undefined;
return () => {
if (t) clearTimeout(t);
t = setTimeout(fn, ms);
};
}
export function EmailUniquenessGuard() {
const { control, setError, clearErrors, getValues } = useFormContext();
const email = useWatch({ control, name: "email" });
const run = useMemo(
() =>
debounce(async () => {
const value = getValues("email");
if (!value) return;
const res = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`);
const data = (await res.json()) as { available: boolean };
if (!data.available) {
setError("email", { type: "validate", message: "Email already exists" });
} else {
clearErrors("email");
}
}, 400),
[clearErrors, getValues, setError]
);
useEffect(() => {
if (!email) return;
run();
}, [email, run]);
return null;
}Praktična pravila:
- debounce između 300 i 600 ms za provjere koje prate tipkanje
- preskočite provjere ako je email lokalno neispravan
- osigurajte da server-side validacija i dalje blokira neispravne submissione
# Korak 5: Višekoračni obrasci bez gubitka stanja#
Višekoračni tokovi pojavljuju se u onboarding-u, checkout-u, zahtjevima za kredit i dugim B2B setup obrascima. Dva najčešća skalabilna modela su:
| Model | Najbolje za | Kompromisi |
|---|---|---|
| Jedno stanje obrasca kroz korake | Jedan finalni payload, manje API poziva | Više client stanja, pažljiva validacija po koraku |
| Spremanje po koraku na server kao draft | Dugi tokovi, compliance, “nastavi kasnije” | Više backend posla, kompleksan lifecycle draftova |
Obrazac: jedna RHF instanca + validacija sheme po koraku#
Koristite jednu useForm instancu i step controller. Validirajte samo polja u trenutnom koraku kako ne biste blokirali napredovanje zbog nepovezanih koraka.
import { z } from "zod";
const Step1Schema = z.object({
firstName: z.string().min(1),
lastName: z.string().min(1),
});
const Step2Schema = z.object({
email: z.string().email(),
phone: z.string().optional(),
});
type StepKey = "profile" | "contact";
const stepFields: Record<StepKey, Array<"firstName" | "lastName" | "email" | "phone">> = {
profile: ["firstName", "lastName"],
contact: ["email", "phone"],
};Zatim u handleru za navigaciju koraka pokrenite validaciju samo za polja tog koraka.
async function nextStep(current: StepKey, trigger: (names: any) => Promise<boolean>) {
const ok = await trigger(stepFields[current]);
return ok;
}Zašto je ovo važno: dugi obrasci propadaju kada korisnici zapnu na koraku zato što greška postoji u skrivenom polju budućeg koraka. Validiranje samo vidljivog koraka je dobitak za konverziju.
ℹ️ Napomena: Za regulirane tokove razmotrite spremanje drafta nakon svakog koraka. Smanjuje gubitak podataka i pomaže support timovima u dijagnostici jer mogu pregledati međustanja.
# Korak 6: Server actions i API integracija uz tipizirano mapiranje grešaka#
Složeni proizvodi trebaju dosljedno ponašanje između:
- client validacije
- server validacije
- database constraints
- third-party API-ja
Najrobusniji obrazac je:
- 1validirajte na klijentu sa Zod-om
- 2pošaljite na server
- 3validirajte ponovno istom shemom na serveru
- 4vratite strukturirane greške
- 5mapirajte greške u RHF s
setError
Error contract#
Zadržite predvidljiv oblik greške.
| Polje | Tip | Značenje |
|---|---|---|
fieldErrors | record | Mapa naziva polja u poruku |
formError | string | Opća poruka greške |
code | string | Opcionalni strojno čitljiv razlog neuspjeha |
Primjer: mapiranje API grešaka u RHF#
type ApiError = {
fieldErrors?: Record<string, string>;
formError?: string;
};
export function applyApiErrors(
err: ApiError,
setError: (name: any, error: any) => void
) {
if (err.formError) {
setError("root", { type: "server", message: err.formError });
}
if (err.fieldErrors) {
for (const [name, message] of Object.entries(err.fieldErrors)) {
setError(name as any, { type: "server", message });
}
}
}Next.js server action obrazac#
U Next.js, server actions su praktične, ali i dalje trebate istu validaciju i isti oblik grešaka.
"use server";
import { CustomerSchema } from "./schemas";
export async function saveCustomerAction(input: unknown) {
const parsed = CustomerSchema.safeParse(input);
if (!parsed.success) {
const fieldErrors: Record<string, string> = {};
for (const issue of parsed.error.issues) {
const key = issue.path.join(".");
fieldErrors[key] = issue.message;
}
return { ok: false, fieldErrors, code: "VALIDATION_ERROR" as const };
}
// Persist to DB here
return { ok: true as const };
}Ovo je važno za pouzdanost proizvoda: korisnici ne bi trebali vidjeti “something went wrong” kad možete ponuditi konkretan popravak na razini polja.
# Korak 7: Najbolje prakse performansi za ogromne obrasce#
Problemi s performansama najčešće dolaze iz nenamjernih re-rendera i “teških” watchera. Koristite ove prakse:
Preferirajte uncontrolled inpute i izbjegavajte globalne pretplate#
- Nemojte zvati
watch()bez popisa specifičnih polja. - Nemojte čitati
formStatena vrhu forme i prosljeđivati ga svuda. - Koristite
useFormStateselektivno po sekciji ako vam trebaisDirtyilierrorsu toj sekciji.
Podijelite obrazac na sekcije#
Velike obrasce treba organizirati po domenskim sekcijama, svaku sa svojim stablom komponenti. To se slaže s obrascima design systema i smanjuje kognitivno opterećenje za developere i reviewere.
Ako želite strategiju skalabilne hijerarhije komponenti, koristite obrasce iz React arhitekture komponenti za skalabilne design sisteme.
Mjerite performanse, ne nagađajte#
Koristite React DevTools Profiler i pratite:
- commitove dok tipkate u polje
- komponente koje se re-renderaju zbog nepovezanih promjena polja
- promjene broja rerenderiranja nakon refaktora
Kao baseline, tipkanje u jedno polje trebalo bi re-renderirati samo:
- to polje
- poruku greške za to polje
- minimalni layout wrapper
# Korak 8: Obrasci pristupačnosti koje biste trebali standardizirati#
Pristupačni obrasci poboljšavaju upotrebljivost za sve i smanjuju rizik. Standardizirajte ova ponašanja u field komponentama:
| Zahtjev | Što napraviti | Zašto je važno |
|---|---|---|
| Povezivanje labela | label vezan uz input preko id | Čitači ekrana najavljuju kontekst |
| Najava greške | poruka greške s role="alert" | Korisnici čuju validacijski feedback |
aria-invalid | postaviti na true kad postoji greška | Signalizira neispravno stanje |
| Described by | aria-describedby pokazuje na pomoć ili grešku | Ispravno čita dodatne naznake |
| Upravljanje fokusom | fokusirati prvo neispravno polje na submit | Manje trenja, pomaže korisnicima tipkovnice |
Također razmotrite kvalitetu teksta. Jasni labeli i help tekst poboljšavaju konverziju i smanjuju konfuziju. Ako vam je važno da se stranice obrazaca dobro indeksiraju i jasno komuniciraju, primijenite SEO i sadržajne prakse prilagođene developerima iz SEO za developere.
# Česte zamke u velikim React obrascima#
- 1
Dupliciranje pravila validacije na više mjesta
Rješenje: Zod neka bude izvor istine, ponovno ga koristite na serveru, a pravila po polju neka budu minimalna. - 2
Validiranje skrivenih polja u višekoračnim tokovima
Rješenje:triggerpo koraku s eksplicitnim popisom polja. - 3
Korištenje
watch()bez ograničenja
Rješenje: koristiteuseWatchza specifična polja i debounceajte remote provjere. - 4
Controllerposvuda
Rješenje: koristiteregisterza nativne inpute, aControllerostavite za zaista controlled komponente. - 5
Server greške se ne mapiraju natrag u UI
Rješenje: definirajte stabilan error contract i mapirajte nasetError, uključujućirootporuku.
# Ključne poruke#
- Koristite schema-first validaciju sa Zod-om i ponovno koristite ista pravila na serveru kako biste spriječili neslaganja i zaobilaženja.
- Izgradite višekratno upotrebljive field komponente koje standardiziraju labele, greške i aria atribute radi brzine i pristupačnosti.
- Implementirajte asinkronu validaciju s debouncingom i uvijek ponovno provjerite na serveru pri submit-u radi ispravnosti.
- Za višekoračne tokove, validirajte samo polja trenutnog koraka kako ne biste blokirali korisnike skrivenim greškama.
- Održite forme brzim tako da izbjegavate globalne watchere i minimizirate re-renderiranja ciljanim pretplatama.
- Vraćajte strukturirane server greške i mapirajte ih na RHF
setErrorza feedback na razini polja i manje support ticketa.
# Zaključak#
Najbolje prakse za React obrasce u velikom mjerilu svode se na jedno načelo: jedan izvor istine za oblik podataka i pravila, uparen s predvidljivim UI gradivnim blokovima. React Hook Form plus Zod daje vam tu osnovu, dok obrasci poput validacije po koraku, debounceanih async provjera i strukturiranog mapiranja server grešaka drže složene proizvode stabilnima i brzima.
Ako gradite veliki Next.js ili React proizvod i obrasci vam postaju usko grlo, Samioda vam može pomoći standardizirati arhitekturu obrazaca, isporučiti sustav field komponenti i učvrstiti server validaciju end-to-end. Javite nam se putem naše stranice i pregledat ćemo vašu trenutnu implementaciju te predložiti pragmatičan plan migracije.
FAQ
Više iz kategorije Web razvoj
Sve →Implementacija Stripe pretplata u Next.js-u: Billing Portal, webhookovi i prava pristupa
Vodič spreman za produkciju za Next.js Stripe pretplate: planovi i probni periodi, Billing Portal, provjera webhookova, idempotentna obrada, mapiranje prava pristupa i testiranje sa Stripe CLI-jem.
Objašnjene strategije cacheiranja u Next.js-u: SSR, SSG, ISR, Route Cache i SWR
Praktičan vodič kroz Next.js strategije cacheiranja u eri App Routera — kako se SSR, SSG, ISR, Route Cache, Data Cache i SWR uklapaju u cjelinu, uz tablice za odluke, primjere koda i česte zamke poput zastarjelih auth i tenant podataka.
Next.js multitenant SaaS arhitektura: modeli tenancije, rutiranje, autentikacija i izolacija podataka (Vodič za 2026.)
Praktičan vodič za Next.js multitenant SaaS arhitekturu: modeli tenancije, tenant-aware rutiranje uz App Router i middleware, obrasci autentikacije te učvršćivanje izolacije podataka kako bi se spriječila curenja.
Trebate pomoć s projektom?
Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.
Povezani članci
Objašnjene strategije cacheiranja u Next.js-u: SSR, SSG, ISR, Route Cache i SWR
Praktičan vodič kroz Next.js strategije cacheiranja u eri App Routera — kako se SSR, SSG, ISR, Route Cache, Data Cache i SWR uklapaju u cjelinu, uz tablice za odluke, primjere koda i česte zamke poput zastarjelih auth i tenant podataka.
Arhitektura React komponenti za skaliranje: obrasci za održiv dizajnerski sustav
Pragmatična arhitektura React komponenti za velike React i Next.js codebaseove: kompozicija, složene (compound) i polimorfne komponente, tematiziranje, konvencije mapa, anti-uzorci i plan refaktoriranja koji vaš tim može pratiti.
Kontrolni popis za migraciju na Next.js App Router (s Pages Routera) + česte zamke
Praktičan, korak-po-korak plan migracije na Next.js App Router s Pages Routera, uključujući kontrolni popis za routing, dohvat podataka, SEO metadata, deployment i vodič za rješavanje čestih zamki.