Web razvoj
ReactObrasciReact Hook FormZodTypeScriptNext.jsPristupačnostPerformanse

React obrasci u velikim aplikacijama: React Hook Form + Zod obrasci za složene proizvode

AO
Adrijan Omićević
·13 min čitanja

# Š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:

SlojOdgovornostPreporučeni obrazac
Zod schemaIzvor istine za oblik i pravilaJedna shema po obrascu i opcionalno po koraku
React Hook FormStanje, praćenje izmjena (dirty), slanje, greškeuseForm + FormProvider + useFormContext
Field komponenteDosljedan UI, labeli, greške, aria povezivanjewrapperi TextField, SelectField, CheckboxField
Async validacijaProvjera jedinstvenosti, udaljena pravilaDebounceani API poziv + setError
Server validacijaKonačni autoritet, sigurnostValidiraj payload istom Zod shemom na serveru
Mapiranje grešakaPretvaranje API grešaka u RHF greškeTipizirani 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#

TypeScript
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”.

TypeScript
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#

TypeScript
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: true pomaže korisnicima tipkovnice i smanjuje trenje.

💡 Savjet: Kad UX zahtijeva trenutnu validaciju, za “teža” pravila radije koristite onBlur nego onChange. 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 name kao tipizirani path
  • renderirati label i povezati ga s inputom
  • renderirati poruku greške s role="alert"
  • postaviti aria-invalid i aria-describedby
  • raditi i s register i s Controller slučajevima

Primjer: TextField s useFormContext#

TypeScript
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#

TypeScript
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:

ModelNajbolje zaKompromisi
Jedno stanje obrasca kroz korakeJedan finalni payload, manje API pozivaViše client stanja, pažljiva validacija po koraku
Spremanje po koraku na server kao draftDugi 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.

TypeScript
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.

TypeScript
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:

  1. 1
    validirajte na klijentu sa Zod-om
  2. 2
    pošaljite na server
  3. 3
    validirajte ponovno istom shemom na serveru
  4. 4
    vratite strukturirane greške
  5. 5
    mapirajte greške u RHF s setError

Error contract#

Zadržite predvidljiv oblik greške.

PoljeTipZnačenje
fieldErrorsrecordMapa naziva polja u poruku
formErrorstringOpća poruka greške
codestringOpcionalni strojno čitljiv razlog neuspjeha

Primjer: mapiranje API grešaka u RHF#

TypeScript
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.

TypeScript
"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 formState na vrhu forme i prosljeđivati ga svuda.
  • Koristite useFormState selektivno po sekciji ako vam treba isDirty ili errors u 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 napravitiZašto je važno
Povezivanje labelalabel vezan uz input preko idČitači ekrana najavljuju kontekst
Najava greškeporuka greške s role="alert"Korisnici čuju validacijski feedback
aria-invalidpostaviti na true kad postoji greškaSignalizira neispravno stanje
Described byaria-describedby pokazuje na pomoć ili greškuIspravno čita dodatne naznake
Upravljanje fokusomfokusirati prvo neispravno polje na submitManje 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. 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. 2

    Validiranje skrivenih polja u višekoračnim tokovima
    Rješenje: trigger po koraku s eksplicitnim popisom polja.

  3. 3

    Korištenje watch() bez ograničenja
    Rješenje: koristite useWatch za specifična polja i debounceajte remote provjere.

  4. 4

    Controller posvuda
    Rješenje: koristite register za nativne inpute, a Controller ostavite za zaista controlled komponente.

  5. 5

    Server greške se ne mapiraju natrag u UI
    Rješenje: definirajte stabilan error contract i mapirajte na setError, uključujući root poruku.

# 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 setError za 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

Share
A
Adrijan OmićevićSamioda Team
All articles →

Trebate pomoć s projektom?

Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.