React Form Validation: React Hook Form + Zod

React Form Validation: React Hook Form + Zod
Form yönetimi ve doğrulama, web uygulamalarının en karmaşık bölümlerinden biridir. React Hook Form performanslı form yönetimi sağlarken, Zod şema bazlı doğrulama ile tip güvenliği sunar. Bu ikili birlikte kullanıldığında, hem kullanıcı deneyimi hem de geliştirici deneyimi açısından mükemmel sonuçlar elde edersiniz. Bu rehberde, bu güçlü kombinasyonu adım adım ele alacağız.
Neden React Hook Form + Zod?
React Hook Form, uncontrolled component yaklaşımıyla minimum re-render sağlar. Zod ise TypeScript-first şema doğrulama kütüphanesi olarak compile-time ve runtime type safety sunar. @hookform/resolvers paketi sayesinde ikisi sorunsuz entegre olur.
Kurulum ve Temel Kullanım
// npm install react-hook-form zod @hookform/resolvers
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Zod şeması — hem doğrulama hem TypeScript tipi
const registerSchema = z.object({
name: z
.string()
.min(2, 'İsim en az 2 karakter olmalıdır')
.max(50, 'İsim en fazla 50 karakter olabilir'),
email: z
.string()
.email('Geçerli bir e-posta adresi giriniz'),
password: z
.string()
.min(8, 'Şifre en az 8 karakter olmalıdır')
.regex(/[A-Z]/, 'En az bir büyük harf içermelidir')
.regex(/[0-9]/, 'En az bir rakam içermelidir')
.regex(/[^A-Za-z0-9]/, 'En az bir özel karakter içermelidir'),
confirmPassword: z.string(),
birthDate: z
.string()
.refine((val) => !isNaN(Date.parse(val)), 'Geçerli bir tarih giriniz')
.refine((val) => new Date(val) < new Date(), 'Doğum tarihi geçmişte olmalıdır'),
terms: z
.boolean()
.refine((val) => val === true, 'Kullanım koşullarını kabul etmelisiniz'),
}).refine((data) => data.password === data.confirmPassword, {
message: 'Şifreler eşleşmiyor',
path: ['confirmPassword'],
});
// Şemadan TypeScript tipi oluştur
type RegisterFormData = z.infer<typeof registerSchema>;
function RegisterForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting, isValid, dirtyFields },
watch,
reset,
} = useForm<RegisterFormData>({
resolver: zodResolver(registerSchema),
mode: 'onBlur', // Alan kaybedildiğinde doğrula
defaultValues: {
name: '',
email: '',
password: '',
confirmPassword: '',
birthDate: '',
terms: false,
},
});
const onSubmit = async (data: RegisterFormData) => {
try {
const response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('Kayıt başarısız');
reset();
alert('Kayıt başarılı!');
} catch (error) {
console.error(error);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate className="space-y-4 max-w-md">
<div>
<label htmlFor="name">İsim</label>
<input id="name" {...register('name')} className={`border rounded px-3 py-2 w-full ${errors.name ? 'border-red-500' : ''}`} />
{errors.name && <p className="text-red-500 text-sm mt-1">{errors.name.message}</p>}
</div>
<div>
<label htmlFor="email">E-posta</label>
<input id="email" type="email" {...register('email')} className={`border rounded px-3 py-2 w-full ${errors.email ? 'border-red-500' : ''}`} />
{errors.email && <p className="text-red-500 text-sm mt-1">{errors.email.message}</p>}
</div>
<div>
<label htmlFor="password">Şifre</label>
<input id="password" type="password" {...register('password')} className={`border rounded px-3 py-2 w-full ${errors.password ? 'border-red-500' : ''}`} />
{errors.password && <p className="text-red-500 text-sm mt-1">{errors.password.message}</p>}
</div>
<div>
<label htmlFor="confirmPassword">Şifre Tekrar</label>
<input id="confirmPassword" type="password" {...register('confirmPassword')} className={`border rounded px-3 py-2 w-full ${errors.confirmPassword ? 'border-red-500' : ''}`} />
{errors.confirmPassword && <p className="text-red-500 text-sm mt-1">{errors.confirmPassword.message}</p>}
</div>
<div>
<label>
<input type="checkbox" {...register('terms')} />
Kullanım koşullarını kabul ediyorum
</label>
{errors.terms && <p className="text-red-500 text-sm mt-1">{errors.terms.message}</p>}
</div>
<button type="submit" disabled={isSubmitting} className="bg-blue-600 text-white px-6 py-2 rounded w-full disabled:opacity-50">
{isSubmitting ? 'Kaydediliyor...' : 'Kayıt Ol'}
</button>
</form>
);
}
mode: 'onBlur' kullanarak, kullanıcı alanı terk ettiğinde doğrulama yapılmasını sağlayın. Bu, kullanıcıyı yazarken rahatsız etmeden zamanında geri bildirim verir. mode: 'onChange' daha agresif bir yaklaşımdır.
Dinamik Form Alanları
import { useForm, useFieldArray } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const orderSchema = z.object({
customerName: z.string().min(2, 'Müşteri adı zorunlu'),
items: z
.array(
z.object({
productName: z.string().min(1, 'Ürün adı zorunlu'),
quantity: z.number().min(1, 'En az 1 adet').max(100, 'En fazla 100 adet'),
unitPrice: z.number().min(0.01, 'Fiyat 0 olamaz'),
})
)
.min(1, 'En az bir ürün eklemelisiniz'),
notes: z.string().optional(),
});
type OrderFormData = z.infer<typeof orderSchema>;
function OrderForm() {
const { register, control, handleSubmit, formState: { errors }, watch } = useForm<OrderFormData>({
resolver: zodResolver(orderSchema),
defaultValues: {
customerName: '',
items: [{ productName: '', quantity: 1, unitPrice: 0 }],
notes: '',
},
});
const { fields, append, remove } = useFieldArray({
control,
name: 'items',
});
const items = watch('items');
const total = items?.reduce((sum, item) => sum + (item.quantity || 0) * (item.unitPrice || 0), 0) || 0;
const onSubmit = (data: OrderFormData) => {
console.log('Sipariş:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label>Müşteri Adı</label>
<input {...register('customerName')} className="border rounded px-3 py-2 w-full" />
{errors.customerName && <p className="text-red-500 text-sm">{errors.customerName.message}</p>}
</div>
<h3>Ürünler</h3>
{fields.map((field, index) => (
<div key={field.id} className="flex gap-2 items-start">
<input {...register(`items.${index}.productName`)} placeholder="Ürün" className="border rounded px-2 py-1 flex-1" />
<input {...register(`items.${index}.quantity`, { valueAsNumber: true })} type="number" placeholder="Adet" className="border rounded px-2 py-1 w-20" />
<input {...register(`items.${index}.unitPrice`, { valueAsNumber: true })} type="number" step="0.01" placeholder="Fiyat" className="border rounded px-2 py-1 w-28" />
{fields.length > 1 && (
<button type="button" onClick={() => remove(index)} className="text-red-500">Sil</button>
)}
</div>
))}
<button type="button" onClick={() => append({ productName: '', quantity: 1, unitPrice: 0 })} className="text-blue-600 underline">
+ Ürün Ekle
</button>
<div className="text-right font-bold">Toplam: {total.toFixed(2)} TL</div>
<button type="submit" className="bg-green-600 text-white px-6 py-2 rounded">Siparişi Oluştur</button>
</form>
);
}
Reusable Form Field Component
import { FieldError, UseFormRegisterReturn } from 'react-hook-form';
interface FormFieldProps {
label: string;
error?: FieldError;
registration: UseFormRegisterReturn;
type?: string;
placeholder?: string;
}
function FormField({ label, error, registration, type = 'text', placeholder }: FormFieldProps) {
return (
<div className="space-y-1">
<label className="block text-sm font-medium text-gray-700">{label}</label>
<input
type={type}
placeholder={placeholder}
{...registration}
className={`block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-2 ${
error
? 'border-red-300 focus:ring-red-500'
: 'border-gray-300 focus:ring-blue-500'
}`}
aria-invalid={error ? 'true' : 'false'}
aria-describedby={error ? `${registration.name}-error` : undefined}
/>
{error && (
<p id={`${registration.name}-error`} className="text-red-500 text-sm" role="alert">
{error.message}
</p>
)}
</div>
);
}
z.infer özelliği, şemadan otomatik TypeScript tipi oluşturur. Bu sayede şema ve tip tanımını tek bir yerde yaparsınız — DRY (Don't Repeat Yourself) prensibi uygulanmış olur.
Sonuç
React Hook Form ve Zod, form yönetiminin en güçlü kombinasyonudur. React Hook Form'un performanslı uncontrolled yaklaşımı ile Zod'un tip güvenli doğrulaması birleştiğinde, hem kullanıcıya hatasız bir deneyim hem de geliştiriciye güvenli bir geliştirme ortamı sunulur. Dinamik alanlar, koşullu doğrulama ve yeniden kullanılabilir field componentleri ile karmaşık formları bile elegant şekilde yönetebilirsiniz.