React Forme i Validacija: React Hook Form i Zod u Produkciji
Nauči kako da praviš robusne, performantne forme u React-u koristeći React Hook Form i Zod — kombinaciju koju koristimo na svakom B2B SaaS projektu u Atonize-u za validaciju, error stanja i async submit bez žrtvovanja performansi.
React Forme i Validacija: React Hook Form i Zod u Produkciji
TL;DR — Ključni Uvidi
- Kontrolisane forme sa
useStatere-renderuju se pri svakom pritisku tastera — React Hook Form to izbegava sa nekontrolisanim input-ima- Zod je TypeScript-first biblioteka za validaciju šema koja ti daje runtime validaciju i inferisane tipove iz jednog izvora istine
- Kombinovanje
react-hook-formsa@hookform/resolvers/zoddaje ti performantne forme sa deklarativnom, type-safe validacijom- Uvek eksplicitno obradi stanja učitavanja, greške i uspešnog submita — posebno u B2B kontekstima gde submit forme okida kritičnu poslovnu logiku
- Server-side validacija je i dalje obavezna — client-side validacija je UX, nije bezbednost
Zašto je upravljanje formama teže nego što izgleda
Forme deluju jednostavno. Input, dugme za submit, gotovo. Ali produkcijske forme u B2B SaaS-u uključuju:
- Validaciju na nivou polja i forme sa specifičnim porukama greške
- Async validaciju (da li ovaj email već postoji?)
- Onemogućavanje dugmeta za submit tokom slanja
- Prikazivanje stanja uspeha i greške nakon submita
- Resetovanje forme nakon uspeha
- TypeScript tipove za vrednosti forme
Sve ovo sa čistim useState i useEffect znači desetine linija boilerplate-a po formi — a u Custom React Hook-ovima smo pokrili kako je ponovljena logika signal za apstrakciju. React Hook Form i Zod su ta apstrakcija, battle-tested u produkciji.
Problem sa kontrolisanim formama
Standardni React pristup formama su kontrolisani input-i — vrednost svakog input-a je vezana za state:
// ❌ Re-renderuje se pri svakom pritisku tastera
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
<input value={email} onChange={(e) => setEmail(e.target.value)} />U formi sa 10 polja, svaki pritisak tastera okida 10+ re-rendera. Za jednostavne forme ovo je neprimetno. Za složene forme sa real-time validacijom kroz mnoga polja, to merljivo degradira performanse.
React Hook Form rešava ovo koristeći nekontrolisane input-e — čita vrednosti direktno iz DOM-a kada je potrebno, umesto da ih sinhronizuje u state pri svakoj promeni.
Postavljanje React Hook Form sa Zod-om
Instaliraj zavisnosti:
npm install react-hook-form zod @hookform/resolversEvo kompletne B2B kontakt/upit forme — kakvu bismo pronašli na SaaS stranici sa cenama ili u RFQ (Request for Quote) sistemu:
// components/InquiryForm.tsx
"use client";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
// Korak 1: Definiši šemu sa Zod-om
const inquirySchema = z.object({
fullName: z.string().min(2, "Ime mora imati najmanje 2 karaktera"),
email: z.string().email("Unesi ispravnu email adresu"),
company: z.string().min(1, "Naziv kompanije je obavezan"),
employees: z.enum(["1-10", "11-50", "51-200", "200+"], {
errorMap: () => ({ message: "Izaberi veličinu kompanije" }),
}),
message: z.string().min(20, "Poruka mora imati najmanje 20 karaktera").max(1000),
});
// Korak 2: Inferiši TypeScript tip iz šeme
type InquiryFormValues = z.infer<typeof inquirySchema>;
export function InquiryForm() {
const [submitStatus, setSubmitStatus] = useState<"idle" | "success" | "error">("idle");
// Korak 3: Inicijalizuj formu
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting },
} = useForm<InquiryFormValues>({
resolver: zodResolver(inquirySchema),
});
// Korak 4: Obradi submit
async function onSubmit(data: InquiryFormValues) {
try {
const res = await fetch("/api/inquiries", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error("Slanje nije uspelo");
setSubmitStatus("success");
reset();
} catch {
setSubmitStatus("error");
}
}
if (submitStatus === "success") {
return (
<div className="rounded-xl border border-green-200 bg-green-50 p-8 text-center">
<p className="text-lg font-semibold text-green-800">Upit primljen!</p>
<p className="mt-2 text-sm text-green-700">
Javićemo se u roku od jednog radnog dana.
</p>
</div>
);
}
return (
<div onSubmit={handleSubmit(onSubmit)} className="space-y-5">
{submitStatus === "error" && (
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
Nešto nije prošlo kako treba. Pokušaj ponovo ili nas kontaktiraj direktno.
</div>
)}
<div className="grid gap-5 sm:grid-cols-2">
<Field label="Ime i prezime" error={errors.fullName?.message}>
<input
{...register("fullName")}
placeholder="Ana Marković"
className={inputClass(!!errors.fullName)}
/>
</Field>
<Field label="Email adresa" error={errors.email?.message}>
<input
{...register("email")}
type="email"
placeholder="ana@kompanija.com"
className={inputClass(!!errors.email)}
/>
</Field>
</div>
<div className="grid gap-5 sm:grid-cols-2">
<Field label="Kompanija" error={errors.company?.message}>
<input
{...register("company")}
placeholder="Acme d.o.o."
className={inputClass(!!errors.company)}
/>
</Field>
<Field label="Veličina kompanije" error={errors.employees?.message}>
<select
{...register("employees")}
className={inputClass(!!errors.employees)}
>
<option value="">Izaberi veličinu...</option>
<option value="1-10">1–10 zaposlenih</option>
<option value="11-50">11–50 zaposlenih</option>
<option value="51-200">51–200 zaposlenih</option>
<option value="200+">200+ zaposlenih</option>
</select>
</Field>
</div>
<Field label="Poruka" error={errors.message?.message}>
<textarea
{...register("message")}
rows={5}
placeholder="Opišite vaš projekat ili zahteve..."
className={inputClass(!!errors.message)}
/>
</Field>
<button
type="submit"
disabled={isSubmitting}
className="w-full rounded-lg bg-blue-600 px-6 py-3 text-sm font-semibold text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
>
{isSubmitting ? "Slanje u toku..." : "Pošalji upit"}
</button>
</div>
);
}
// Helper komponente
function Field({
label,
error,
children,
}: {
label: string;
error?: string;
children: React.ReactNode;
}) {
return (
<div className="space-y-1.5">
<label className="block text-sm font-medium text-gray-700">{label}</label>
{children}
{error && <p className="text-xs text-red-600">{error}</p>}
</div>
);
}
function inputClass(hasError: boolean): string {
return [
"w-full rounded-lg border px-4 py-2.5 text-sm outline-none transition",
"focus:ring-2 focus:ring-blue-500 focus:ring-offset-0",
hasError
? "border-red-300 bg-red-50 focus:ring-red-400"
: "border-gray-300 bg-white focus:border-blue-500",
].join(" ");
}Zašto je Zod + React Hook Form prava kombinacija
Ključni uvid je jedan izvor istine za validaciju:
- Jednom definišeš šemu u Zod-u
- Zod ti daje runtime validaciju (hvata loše podatke)
z.infer<typeof schema>ti automatski daje TypeScript tip- Ista šema može biti korišćena na serveru (u API ruti) za server-side validaciju
// app/api/inquiries/route.ts — ista šema, ista validacija, server-side
import { inquirySchema } from "@/lib/schemas/inquiry";
export async function POST(req: Request) {
const body = await req.json();
const result = inquirySchema.safeParse(body);
if (!result.success) {
return Response.json({ errors: result.error.flatten() }, { status: 400 });
}
// result.data je potpuno tipiziran i validiran
await db.inquiries.create({ data: result.data });
return Response.json({ success: true });
}Ovo je end-to-end type safety: ista Zod šema validira formu na klijentu i API payload na serveru.
Česte Greške i Zamke
1. Preskakanje server-side validacije
Client-side validacija je UX feature — daje trenutni feedback. Nije sigurnosna mera. Svako je može zaobići direktnim API pozivom. Uvek validiraj i na serveru, idealno istom Zod šemom.
2. Neobrada svih stanja submita
Čest propust je obrada samo uspeha. Uvek obradi sva tri:
// ✅ Pokrij sva stanja
const [submitStatus, setSubmitStatus] = useState<"idle" | "success" | "error">("idle");U B2B formama koje okidaju emailove, CRM unose ili tokove plaćanja, neobrađeno stanje greške ostavlja korisnike zbunjene i support inboxeve pune.
3. Zaboravljanje resetovanja nakon uspeha
const { reset } = useForm();
async function onSubmit(data) {
await submitToAPI(data);
reset(); // ← ne zaboravi ovo
}Bez reset(), polja forme zadržavaju vrednosti nakon submita — zbunjujuće za korisnika i izvor slučajnih ponovnih slanja.
4. Previše generičke poruke greške
// ❌ Beskorisno
message: z.string().min(1, "Neispravno")
// ✅ Akciono
message: z.string().min(20, "Poruka mora imati najmanje 20 karaktera")Poruke greške treba da korisniku tačno kažu šta da ispravi. U B2B kontekstima, forme često popunjavaju netehničke osobe — jasnoća je važna.
Često Postavljana Pitanja (FAQ)
Zašto koristiti React Hook Form umesto useState?
React Hook Form izbegava re-renderovanje pri svakom pritisku tastera koristeći nekontrolisane input-e. Takođe pruža konzistentan API za registraciju, validaciju, error stanja i submit forme koji bi inače zahtevao značajan custom kod. Za jednostavne forme sa 2-3 polja, useState je sasvim ok. Za sve složenije, React Hook Form se isplati odmah.
Mogu li koristiti Zod bez React Hook Form?
Apsolutno. Zod je samostalna biblioteka za validaciju. Možeš je koristiti za validaciju API odgovora, parsovanje environment varijabli, validaciju query parametara, ili validaciju bilo kojih podataka u runtime-u. Integracija sa React Hook Form putem @hookform/resolvers je samo jedan od mnogih slučajeva upotrebe.
Kako da rukovam greškama vraćenim sa servera?
React Hook Form-ova funkcija setError ti omogućava da programski postaviš greške na specifičnim poljima iz tvog API odgovora:
const { setError } = useForm();
// Ako API vrati { field: "email", message: "Već se koristi" }
setError("email", { message: "Ovaj email je već registrovan" });Šta je sa upload-om fajlova u React Hook Form-u?
File input-i rade sa register-om, ali zahtevaju posebno rukovanje jer fajlovi ne mogu biti serijalizovani u JSON. Koristi watch("fileField") da dobiješ FileList, a zatim ručno dodaj u FormData u svom submit handler-u umesto slanja JSON-a.
Serijal: React Masterclass
- 1
- 2
- 3
- 4
- 5
- 6
- 7React Forme i Validacija: React Hook Form i Zod u Produkciji (You are here)