04 / 09
React Hook Form ile Zod
zodResolver ile şemayı forma bağlamak, kurulumdaki npm uyarısı, tiplerin şemadan çıkması, transform ile input ve output tipleri, alanlar arası kuralda deps ve şemayı sunucuyla paylaşmak.
Neden Birlikte?
React Hook Form temelleri dersinde kuralları register'a yazdınız. Bu yaklaşımın iki sorunu var: kurallar JSX'in içine dağılır ve aynı kuralları sunucuda tekrar yazmanız gerekir. Çözüm kuralları bir Zod şemasına taşıyıp formu bu şemayla doğrulamaktır. Bağlantıyı resolver kurar.12
Kurulum
$ pnpm add react-hook-form zod @hookform/resolversUyarı
@hookform/resolvers 6.0.0 (19 Ekim 2026) npm 10'da ERESOLVE unable to resolve dependency tree hatası verebilir. Paket desteklediği her doğrulama kütüphanesini isteğe bağlı peer olarak listeler ve npm 10 bunların arasındaki bir sürüm çakışmasını çözemez. Node.js 24 ile gelen npm 11 ve pnpm sorunsuz kurar. npm 10'da kalmanız gerekiyorsa --legacy-peer-deps geçici bir çözümdür.3
zodResolver
Şema bir dosyada, form başka bir dosyada durur:2
// src/features/auth/sign-up-schema.ts
import { z } from "zod";
export const signUpSchema = z
.object({
name: z.string().trim().min(2, "Ad en az 2 karakter olmalı."),
email: z.email("E-posta adresini ad@ornek.com biçiminde girin."),
password: z.string().min(8, "Şifre en az 8 karakter olmalı."),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
error: "Şifreler eşleşmiyor.",
path: ["confirmPassword"],
});
export type SignUpValues = z.infer<typeof signUpSchema>;// src/features/auth/sign-up-form.tsx
import { useForm, type SubmitHandler } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { signUpSchema, type SignUpValues } from "./sign-up-schema";
export function SignUpForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(signUpSchema),
defaultValues: { name: "", email: "", password: "", confirmPassword: "" },
mode: "onTouched",
});
const onSubmit: SubmitHandler<SignUpValues> = (values) => createAccount(values);
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<label htmlFor="name">Ad</label>
<input id="name" autoComplete="name" {...register("name")} />
{errors.name && <p>{errors.name.message}</p>}
<label htmlFor="email">E-posta</label>
<input id="email" type="email" autoComplete="email" {...register("email")} />
{errors.email && <p>{errors.email.message}</p>}
<label htmlFor="password">Şifre</label>
<input
id="password"
type="password"
autoComplete="new-password"
{...register("password", { deps: "confirmPassword" })}
/>
{errors.password && <p>{errors.password.message}</p>}
<label htmlFor="confirmPassword">Şifre tekrar</label>
<input
id="confirmPassword"
type="password"
autoComplete="new-password"
{...register("confirmPassword")}
/>
{errors.confirmPassword && <p>{errors.confirmPassword.message}</p>}
<button>Hesap oluştur</button>
</form>
);
}useForm'a generic vermediniz; alan adları ve errors'ın tipi zodResolver(signUpSchema)'dan çıkar. register("emial") gibi bir yazım hatası derlemede yakalanır.3
Resolver'la çalışırken üç kural:1
Yerleşik kurallar çalışmaz.
register("name", { required: true })resolver varken yok sayılır. Tüm kurallar şemada olmalı.Form düzeyindeki
validateçalışmaz. RHF 7.72 ile gelenuseForm({ validate })ve resolver birbirini dışlar; ikisi birden verilirse yalnızca resolver çalışır.Hata mesajı şemadan gelir. Mesajları şemaya yazın; JSX yalnızca
errors.x.message'ı gösterir.
Alanlar Arası Kural ve deps
Şifre ve tekrarı eşleştiğinde hata yok. Kullanıcı sonra şifreyi değiştirirse "Şifreler eşleşmiyor." çıkmalı. Çıkmaz: RHF yalnızca değişen alanın hatasını günceller; refine confirmPassword'a yazıyor ama değişen alan password.
deps bu bağı kurar. password doğrulandığında listedeki alanlar da yeniden doğrulanır:4
<input type="password" {...register("password", { deps: "confirmPassword" })} />Yukarıdaki formda deps bu yüzden var. trigger() ile elle doğrulama yaptığınızda deps uygulanmaz.4
İpucu
Zod'da nesnenin .refine'ı, nesnenin herhangi bir alanında tip hatası varken çalışmaz. Seçilmemiş bir z.enum alanı şifre kontrolünü bekletebilir (bkz. Zod ile şema ve doğrulama).
Input ve Output Tipleri
Şemada .transform varsa şemanın girdi ve çıktı tipi ayrılır. Form girdi tipini tutar (input'taki metin), handleSubmit çıktı tipini verir (dönüşmüş değer):35
const productSchema = z.object({
title: z.string().trim().min(1, "Ürün adı girin."),
price: z
.string()
.regex(/^\d+(,\d{1,2})?$/, "Fiyatı 1250,50 biçiminde girin.")
.transform((value) => Number(value.replace(",", "."))),
});
type ProductInput = z.input<typeof productSchema>; // { title: string; price: string }
type ProductOutput = z.output<typeof productSchema>; // { title: string; price: number }
export function ProductForm() {
const { register, handleSubmit } = useForm<ProductInput, unknown, ProductOutput>({
resolver: zodResolver(productSchema),
defaultValues: { title: "", price: "" },
});
return (
<form onSubmit={handleSubmit((product) => saveProduct(product))}>
<input {...register("title")} />
<input inputMode="decimal" {...register("price")} />
</form>
);
}useForm'un üç generic'i sırasıyla form değerleri, context ve gönderilen değerlerdir. Ya hiç generic vermeyin ve tipi resolver'dan çıkarın, ya da üçünü birden verin. Tek generic useForm<ProductOutput> iki tipi aynıya sabitler ve zodResolver ile çakışır. Resolver dokümanı .default() için de aynı uyarıyı yapar: varsayılan değeri olan alan girdide isteğe bağlı, çıktıda zorunludur.3
Şemayı Sunucuyla Paylaşmak
Şemayı ayrı bir dosyada tutmanın asıl faydası sunucudadır. Tarayıcıdaki doğrulama kullanıcı deneyimi içindir; atlatılabilir. Sunucu aynı şemayla girdiyi tekrar doğrular:67
// app/sign-up/actions.ts
"use server";
import { signUpSchema } from "@/features/auth/sign-up-schema";
export async function createAccount(values: unknown) {
const parsed = signUpSchema.safeParse(values);
if (!parsed.success) {
return { status: "error" as const };
}
// parsed.data güvenli
}Hata
Şemada .transform varsa dikkat: handleSubmit dönüşmüş değeri verir. Bunu aynı şemayla sunucuda tekrar doğrularsanız şema metin bekler ama sayı gelir ve doğrulama başarısız olur. İki yol var: sunucuya gidecek şemada dönüşüm yapmayın ve dönüşümü sunucuda yapın, ya da sunucuya ham FormData gönderin. Ayrıntılar Server Actions ile form dersinde.
Kendin Dene
Kayıt formunu kurun. Şifreleri eşleştirin, sonra şifreyi değiştirin.
deps'i silip tekrar deneyin.register("name", { required: "Zorunlu" })ekleyip şemadanmin(2)'yi silin. Boş gönderince ne oluyor?ProductForm'da generic'leri silin.product.price'ın tipi ne oluyor? Sonra yalnızcauseForm<ProductOutput>yazın ve hatayı okuyun.createAccount'ı formu kullanmadan, bir test butonundancreateAccount({ email: "x" })ile çağırın. Sunucu ne döndürüyor?
Detaylı Öğrenmek İçin
Resmi doküman: React Hook Form, "Schema Validation"; resolver ile şema doğrulaması.2
Repo:
@hookform/resolversREADME; Zod örneği ve input/output tipleri.3Resmi doküman: React Hook Form, "useForm: resolver"; resolver kuralları.1
Kaynaklar
Resolver, yerleşik kurallarla birlikte kullanılamaması ve form düzeyindeki `validate` ile birbirini dışlaması.
Şema doğrulaması ve resolver kurulumu.
`zodResolver`, desteklenen kütüphaneler, `.default()` ve üç generic uyarısı.
`deps` seçeneği ve `trigger` ile ilişkisi.
`.transform`, `z.input` ve `z.output`.
Her zaman sunucuda doğrulamak, tarayıcı doğrulamasının atlatılabilmesi.
Server Action'da şema ile doğrulama.
Son güncelleme tarihi: 22 Ekim 2026
