Skip to content

Ders 09: Form Yönetimi ve Validasyon

  • Form state yönetimi
  • Zod ile şema validasyonu
  • Client vs server validasyon
  • Form submission handling
  • Form verisi saklama
  • Error handling ve kullanıcı geri bildirimi
  • React Hook Forms entegrasyonu

Form yönetimi, kullanıcı girdilerini toplama, doğrulama ve gönderme işlemidir.

BileşenAçıklama
StateForm alanlarının değerleri
ValidationGirdilerin kurallara uygunluğu
SubmissionFormun gönderilmesi
ErrorsValidasyon hataları
FeedbackKullanıcıya bilgi verme

import { createFileRoute } from '@tanstack/react-router'
import React from 'react'
export const Route = createFileRoute('/iletisim')({
component: IletisimPage,
})
function IletisimPage() {
// Form state'i
const [form, setForm] = React.useState({
ad: '',
email: '',
mesaj: '',
})
// Hata state'i
const [hatalar, setHatalar] = React.useState<{
ad?: string
email?: string
mesaj?: string
}>({})
// Yükleniyor durumu
const [gonderiliyor, setGonderiliyor] = React.useState(false)
const [basari, setBasari] = React.useState(false)
// Input değişikliği
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
// State güncelle
setForm((onceki) => ({ ...onceki, [name]: value }))
// Alan için hatayı temizle
setHatalar((onceki) => ({ ...onceki, [name]: undefined }))
}
// Validasyon
const validate = () => {
const yeniHatalar: typeof hatalar = {}
// Ad validasyonu
if (!form.ad.trim()) {
yeniHatalar.ad = 'Ad zorunludur'
} else if (form.ad.length < 3) {
yeniHatalar.ad = 'Ad en az 3 karakter olmalı'
}
// Email validasyonu
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!form.email.trim()) {
yeniHatalar.email = 'Email zorunludur'
} else if (!emailRegex.test(form.email)) {
yeniHatalar.email = 'Geçerli bir email girin'
}
// Mesaj validasyonu
if (!form.mesaj.trim()) {
yeniHatalar.mesaj = 'Mesaj zorunludur'
} else if (form.mesaj.length < 10) {
yeniHatalar.mesaj = 'Mesaj en az 10 karakter olmalı'
}
setHatalar(yeniHatalar)
return Object.keys(yeniHatalar).length === 0
}
// Form gönderme
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// Validasyon
if (!validate()) {
return
}
setGonderiliyor(true)
try {
// API çağrısı
const response = await fetch('/api/iletisim', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
})
if (!response.ok) {
throw new Error('Bir hata oluştu')
}
setBasari(true)
setForm({ ad: '', email: '', mesaj: '' })
// 3 saniye sonra başarı mesajını kaldır
setTimeout(() => setBasari(false), 3000)
} catch (error: any) {
alert('Hata: ' + error.message)
} finally {
setGonderiliyor(false)
}
}
return (
<div style={{ maxWidth: '600px', margin: '2rem auto', padding: '0 1rem' }}>
<h1 style={{ fontSize: '2rem', marginBottom: '1.5rem' }}>İletişim Formu</h1>
{basari && (
<div style={{
padding: '1rem',
backgroundColor: '#d1fae5',
color: '#065f46',
borderRadius: '4px',
marginBottom: '1rem',
}}>
Mesajınız başarıyla gönderildi!
</div>
)}
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
{/* Ad Alanı */}
<div>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500' }}>
Ad Soyad *
</label>
<input
type="text"
name="ad"
value={form.ad}
onChange={handleChange}
placeholder="Adınızı girin"
style={{
width: '100%',
padding: '0.75rem',
border: hatalar.ad ? '1px solid #dc2626' : '1px solid #d1d5db',
borderRadius: '4px',
fontSize: '1rem',
}}
/>
{hatalar.ad && (
<p style={{ color: '#dc2626', fontSize: '0.875rem', marginTop: '0.25rem' }}>
{hatalar.ad}
</p>
)}
</div>
{/* Email Alanı */}
<div>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500' }}>
Email *
</label>
<input
type="email"
name="email"
value={form.email}
onChange={handleChange}
placeholder="ornek@email.com"
style={{
width: '100%',
padding: '0.75rem',
border: hatalar.email ? '1px solid #dc2626' : '1px solid #d1d5db',
borderRadius: '4px',
fontSize: '1rem',
}}
/>
{hatalar.email && (
<p style={{ color: '#dc2626', fontSize: '0.875rem', marginTop: '0.25rem' }}>
{hatalar.email}
</p>
)}
</div>
{/* Mesaj Alanı */}
<div>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500' }}>
Mesaj *
</label>
<textarea
name="mesaj"
value={form.mesaj}
onChange={handleChange}
placeholder="Mesajınızı yazın..."
rows={5}
style={{
width: '100%',
padding: '0.75rem',
border: hatalar.mesaj ? '1px solid #dc2626' : '1px solid #d1d5db',
borderRadius: '4px',
fontSize: '1rem',
resize: 'vertical',
}}
/>
{hatalar.mesaj && (
<p style={{ color: '#dc2626', fontSize: '0.875rem', marginTop: '0.25rem' }}>
{hatalar.mesaj}
</p>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={gonderiliyor}
style={{
padding: '0.875rem 1.5rem',
backgroundColor: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '4px',
fontSize: '1rem',
fontWeight: '500',
cursor: gonderiliyor ? 'not-allowed' : 'pointer',
opacity: gonderiliyor ? 0.5 : 1,
}}
>
{gonderiliyor ? 'Gönderiliyor...' : 'Gönder'}
</button>
</form>
</div>
)
}

Zod, TypeScript-first bir validasyon kütüphanesidir.

src/lib/validations.ts
import { z } from 'zod'
// İletişim formu şeması
export const iletisimSchema = z.object({
ad: z
.string()
.min(1, 'Ad zorunludur')
.min(3, 'Ad en az 3 karakter olmalı')
.max(50, 'Ad en fazla 50 karakter olabilir'),
email: z
.string()
.min(1, 'Email zorunludur')
.email('Geçerli bir email girin'),
mesaj: z
.string()
.min(1, 'Mesaj zorunludur')
.min(10, 'Mesaj en az 10 karakter olmalı')
.max(1000, 'Mesaj en fazla 1000 karakter olabilir'),
// İsteğe bağlı alan
telefon: z
.string()
.regex(/^(\+90)?[0-9]{10}$/, 'Geçerli bir telefon numarası girin')
.optional(),
})
// Kayıt formu şeması
export const kayitSchema = z.object({
ad: z.string().min(1).min(3),
email: z.string().email(),
sifre: z.string().min(8, 'Şifre en az 8 karakter'),
sifreTekrar: z.string(),
}).refine((data) => data.sifre === data.sifreTekrar, {
message: 'Şifreler eşleşmiyor',
path: ['sifreTekrar'],
})
// Ürün formu şeması
export const urunSchema = z.object({
baslik: z.string().min(5).max(100),
aciklama: z.string().min(20).max(500),
fiyat: z.number().positive('Fiyat pozitif olmalı'),
kategori: z.enum(['elektronik', 'giyim', 'ev', 'spor']),
stok: z.number().int().min(0),
aktif: z.boolean().default(true),
})
// Type inference
export type IletisimForm = z.infer<typeof iletisimSchema>
export type KayitForm = z.infer<typeof kayitSchema>
export type UrunForm = z.infer<typeof urunSchema>
src/lib/validate.ts
import { iletisimSchema, type IletisimForm } from './validations'
export function validateIletisimForm(data: unknown) {
const result = iletisimSchema.safeParse(data)
if (!result.success) {
// Hataları formatla
const hatalar: Record<string, string> = {}
result.error.issues.forEach((issue) => {
if (issue.path[0]) {
hatalar[issue.path[0].toString()] = issue.message
}
})
return { basari: false, hatalar }
}
return { basari: true, data: result.data }
}
// Kullanımı
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const result = validateIletisimForm(form)
if (!result.basari) {
setHatalar(result.hatalar)
return
}
// Valid başarılı, result.data kullan
console.log(result.data)
}

src/lib/iletisim-server.ts
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
const iletisimServerSchema = z.object({
ad: z.string().min(3),
email: z.string().email(),
mesaj: z.string().min(10),
})
export const iletisimGonder = createServerFn({ method: 'POST' })
.inputValidator(iletisimServerSchema)
.handler(async ({ data }) => {
// Zod validasyonu otomatik yapılır
// Burada data tipi: z.infer<typeof iletisimServerSchema>
// Veritabanına kaydet
// await db.iletisim.create({ data })
// Email gönder
// await sendEmail({ to: 'admin@example.com', ...data })
return {
basari: true,
mesaj: 'Mesajınız başarıyla gönderildi',
}
})
src/routes/iletisim.tsx
import { createFileRoute } from '@tanstack/react-router'
import { useServerFn } from '@tanstack/react-start'
import { iletisimGonder } from '../../lib/iletisim-server'
import React from 'react'
export const Route = createFileRoute('/iletisim')({
component: IletisimPage,
})
function IletisimPage() {
const iletisimGonderFn = useServerFn(iletisimGonder)
const [form, setForm] = React.useState({
ad: '',
email: '',
mesaj: '',
})
const [hatalar, setHatalar] = React.useState<Record<string, string>>({})
const [gonderiliyor, setGonderiliyor] = React.useState(false)
const [basari, setBasari] = React.useState(false)
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setForm((p) => ({ ...p, [name]: value }))
setHatalar((p) => ({ ...p, [name]: undefined }))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setGonderiliyor(true)
setHatalar({})
try {
await iletisimGonderFn({ data: form })
setBasari(true)
setForm({ ad: '', email: '', mesaj: '' })
} catch (error: any) {
// Server-side hataları handle et
if (error.errors) {
setHatalar(error.errors)
} else {
setHatalar({ general: error.message })
}
} finally {
setGonderiliyor(false)
}
}
return (
<div style={{ maxWidth: '600px', margin: '2rem auto', padding: '0 1rem' }}>
<h1>İletişim</h1>
{basari && (
<div style={{ padding: '1rem', backgroundColor: '#d1fae5', marginBottom: '1rem' }}>
Mesajınız gönderildi!
</div>
)}
{hatalar.general && (
<div style={{ padding: '1rem', backgroundColor: '#fee2e2', marginBottom: '1rem' }}>
{hatalar.general}
</div>
)}
<form onSubmit={handleSubmit}>
{/* Alanlar... */}
</form>
</div>
)
}

Form yönetimi için kendi hook’unuzu oluşturun.

src/hooks/useForm.ts
import React from 'react'
import { z } from 'zod'
type FormState<T> = {
data: T
errors: Record<keyof T, string | undefined>
touched: Record<keyof T, boolean>
}
type UseFormOptions<T> = {
initialValues: T
schema?: z.ZodSchema<T>
onSubmit: (data: T) => Promise<void> | void
}
export function useForm<T extends Record<string, any>>({
initialValues,
schema,
onSubmit,
}: UseFormOptions<T>) {
const [form, setForm] = React.useState<FormState<T>>({
data: initialValues,
errors: {} as any,
touched: {} as any,
})
const [isSubmitting, setIsSubmitting] = React.useState(false)
const [success, setSuccess] = React.useState(false)
const handleChange = (name: keyof T, value: any) => {
setForm((prev) => ({
...prev,
data: { ...prev.data, [name]: value },
touched: { ...prev.touched, [name]: true },
}))
// Alan error'unu temizle
if (form.errors[name]) {
setForm((prev) => ({
...prev,
errors: { ...prev.errors, [name]: undefined },
}))
}
}
const validate = async (): Promise<boolean> => {
if (!schema) return true
const result = await schema.safeParseAsync(form.data)
if (!result.success) {
const errors: Record<keyof T, string | undefined> = {} as any
result.error.issues.forEach((issue) => {
const field = issue.path[0] as keyof T
errors[field] = issue.message
})
setForm((prev) => ({ ...prev, errors }))
return false
}
setForm((prev) => ({ ...prev, errors: {} as any }))
return true
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// Tüm alanları touched yap
setForm((prev) => {
const touched = {} as Record<keyof T, boolean>
Object.keys(prev.data).forEach((key) => {
touched[key as keyof T] = true
})
return { ...prev, touched }
})
const isValid = await validate()
if (!isValid) return
setIsSubmitting(true)
try {
await onSubmit(form.data)
setSuccess(true)
setForm({
data: initialValues,
errors: {} as any,
touched: {} as any,
})
} catch (error: any) {
console.error('Form submission error:', error)
} finally {
setIsSubmitting(false)
}
}
const reset = () => {
setForm({
data: initialValues,
errors: {} as any,
touched: {} as any,
})
setSuccess(false)
}
return {
data: form.data,
errors: form.errors,
touched: form.touched,
isSubmitting,
success,
handleChange,
handleSubmit,
reset,
}
}
src/routes/kayit.tsx
import { useForm } from '../hooks/useForm'
import { kayitSchema } from '../lib/validations'
function KayitPage() {
const {
data,
errors,
touched,
isSubmitting,
success,
handleChange,
handleSubmit,
reset,
} = useForm({
initialValues: {
ad: '',
email: '',
sifre: '',
sifreTekrar: '',
},
schema: kayitSchema,
onSubmit: async (data) => {
// API çağrısı
await fetch('/api/kayit', {
method: 'POST',
body: JSON.stringify(data),
})
},
})
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={data.ad}
onChange={(e) => handleChange('ad', e.target.value)}
/>
{touched.ad && errors.ad && <span>{errors.ad}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Kaydediliyor...' : 'Kayıt Ol'}
</button>
</form>
)
}

Multi-step form (wizard), birden fazla adımdan oluşan formlardır.

src/routes/satis/siparis.tsx
import { createFileRoute } from '@tanstack/react-router'
import React from 'react'
type FormData = {
// Adım 1: Ürün seçimi
urunId: string
adet: number
// Adım 2: Teslimat
adres: string
sehir: string
postaKodu: string
// Adım 3: Ödeme
kartNumarasi: string
sonKullanma: string
cvv: string
}
const BASLANGIC_FORM: FormData = {
urunId: '',
adet: 1,
adres: '',
sehir: '',
postaKodu: '',
kartNumarasi: '',
sonKullanma: '',
cvv: '',
}
export const Route = createFileRoute('/satis/siparis')({
component: SiparisPage,
})
function SiparisPage() {
const [adim, setAdim] = React.useState(1)
const [form, setForm] = React.useState<FormData>(BASLANGIC_FORM)
const [gonderiliyor, setGonderiliyor] = React.useState(false)
const [basari, setBasari] = React.useState(false)
const handleChange = (field: keyof FormData, value: any) => {
setForm((p) => ({ ...p, [field]: value }))
}
const adimGec = (yeniAdim: number) => {
// Validasyon (basit)
if (yeniAdim > adim) {
if (adim === 1 && !form.urunId) {
alert('Lütfen bir ürün seçin')
return
}
if (adim === 2 && (!form.adres || !form.sehir)) {
alert('Lütfen adres bilgilerini doldurun')
return
}
}
setAdim(yeniAdim)
window.scrollTo(0, 0)
}
const handleSubmit = async () => {
setGonderiliyor(true)
// API çağrısı
await new Promise((resolve) => setTimeout(resolve, 2000))
setBasari(true)
setGonderiliyor(false)
}
if (basari) {
return (
<div style={{ textAlign: 'center', padding: '3rem' }}>
<h1>✅ Siparişiniz Alındı!</h1>
<p>Sipariş numaranız: #{Math.random().toString(36).substr(2, 9).toUpperCase()}</p>
</div>
)
}
return (
<div style={{ maxWidth: '800px', margin: '2rem auto', padding: '0 1rem' }}>
<h1>Sipariş Oluştur</h1>
{/* Progress Bar */}
<div style={{ display: 'flex', marginBottom: '2rem', gap: '0.5rem' }}>
{[1, 2, 3].map((i) => (
<div
key={i}
style={{
flex: 1,
height: '4px',
backgroundColor: i <= adim ? '#3b82f6' : '#e5e7eb',
borderRadius: '2px',
}}
/>
))}
</div>
{/* Adım 1: Ürün */}
{adim === 1 && (
<div>
<h2>1. Ürün Seçimi</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<label>
Ürün:
<select
value={form.urunId}
onChange={(e) => handleChange('urunId', e.target.value)}
style={{ width: '100%', padding: '0.75rem', marginTop: '0.5rem' }}
>
<option value="">Seçin...</option>
<option value="1">Laptop - ₺25,000</option>
<option value="2">Mouse - ₺500</option>
<option value="3">Klavye - ₺1,500</option>
</select>
</label>
<label>
Adet:
<input
type="number"
min="1"
max="10"
value={form.adet}
onChange={(e) => handleChange('adet', parseInt(e.target.value))}
style={{ width: '100%', padding: '0.75rem', marginTop: '0.5rem' }}
/>
</label>
</div>
<button
onClick={() => adimGec(2)}
style={{
marginTop: '1.5rem',
padding: '0.75rem 1.5rem',
backgroundColor: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Devam Et →
</button>
</div>
)}
{/* Adım 2: Teslimat */}
{adim === 2 && (
<div>
<h2>2. Teslimat Adresi</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<label>
Adres:
<textarea
value={form.adres}
onChange={(e) => handleChange('adres', e.target.value)}
rows={3}
style={{ width: '100%', padding: '0.75rem', marginTop: '0.5rem' }}
/>
</label>
<label>
Şehir:
<input
type="text"
value={form.sehir}
onChange={(e) => handleChange('sehir', e.target.value)}
style={{ width: '100%', padding: '0.75rem', marginTop: '0.5rem' }}
/>
</label>
<label>
Posta Kodu:
<input
type="text"
value={form.postaKodu}
onChange={(e) => handleChange('postaKodu', e.target.value)}
style={{ width: '100%', padding: '0.75rem', marginTop: '0.5rem' }}
/>
</label>
</div>
<div style={{ display: 'flex', gap: '1rem', marginTop: '1.5rem' }}>
<button
onClick={() => adimGec(1)}
style={{
padding: '0.75rem 1.5rem',
backgroundColor: '#9ca3af',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
← Geri
</button>
<button
onClick={() => adimGec(3)}
style={{
padding: '0.75rem 1.5rem',
backgroundColor: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Devam Et →
</button>
</div>
</div>
)}
{/* Adım 3: Ödeme */}
{adim === 3 && (
<div>
<h2>3. Ödeme Bilgileri</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<label>
Kart Numarası:
<input
type="text"
placeholder="1234 5678 9012 3456"
value={form.kartNumarasi}
onChange={(e) => handleChange('kartNumarasi', e.target.value)}
style={{ width: '100%', padding: '0.75rem', marginTop: '0.5rem' }}
/>
</label>
<div style={{ display: 'flex', gap: '1rem' }}>
<label style={{ flex: 1 }}>
Son Kullanma:
<input
type="text"
placeholder="AA/YY"
value={form.sonKullanma}
onChange={(e) => handleChange('sonKullanma', e.target.value)}
style={{ width: '100%', padding: '0.75rem', marginTop: '0.5rem' }}
/>
</label>
<label style={{ flex: 1 }}>
CVV:
<input
type="text"
placeholder="123"
value={form.cvv}
onChange={(e) => handleChange('cvv', e.target.value)}
style={{ width: '100%', padding: '0.75rem', marginTop: '0.5rem' }}
/>
</label>
</div>
</div>
<div style={{ display: 'flex', gap: '1rem', marginTop: '1.5rem' }}>
<button
onClick={() => adimGec(2)}
style={{
padding: '0.75rem 1.5rem',
backgroundColor: '#9ca3af',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
← Geri
</button>
<button
onClick={handleSubmit}
disabled={gonderiliyor}
style={{
padding: '0.75rem 1.5rem',
backgroundColor: '#10b981',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: gonderiliyor ? 'not-allowed' : 'pointer',
opacity: gonderiliyor ? 0.5 : 1,
}}
>
{gonderiliyor ? 'İşleniyor...' : 'Siparişi Tamamla ✓'}
</button>
</div>
</div>
)}
</div>
)
}

Bu derste öğrendiklerimiz:

KonuAçıklama
Controlled componentsForm alanları state ile yönetilir
Uncontrolled componentsref ile değer alınır
ZodTypeScript-first validasyon
Server validationServer function’lar ile validasyon
Form hooksÖzel hook’larla form yönetimi
Multi-step formsAdım adım formlar
Error handlingHata mesajları ve geri bildirim
StratejiZamanAvantaj
Client validationAnındaHızlı geri bildirim
Server validationSubmit’teGüvenli, veri tutarlılığı
HybridHer ikisi deEn iyi UX + güvenlik

Ürün ekleme formu oluşturun:

// Başlık, açıklama, fiyat, stok, kategori
// Resim yükleme
// Zod validasyonu

Filtreleme ile arama formu:

// Anahtar kelime, kategori, fiyat aralığı
// URL search params'a kaydet
// Enter'a basınca ara

Form verisi taslak olarak saklayın:

// localStorage'a kaydet
// Sayfa yenilendiğinde geri yükle
// "Taslağı sil" butonu

🚀 Sonraki Ders: Deployment ve Produksiyon

Section titled “🚀 Sonraki Ders: Deployment ve Produksiyon”

Bir sonraki derste şunları öğreneceksiniz:

  • 🐳 Uygulamayı build alma
  • ☁️ Deploy seçenekleri (Vercel, Netlify, Docker)
  • 🌧️ Environment variables yönetimi
  • 🔍 Hata ayıklama ve monitoring
  • ⚡ Performans optimizasyonu
  • 📊 Analytics entegrasyonu

  1. Client vs server validasyon hangisi daha iyi?
  2. Form verisi nerede saklanmalı?
  3. Multi-step form nasıl test edilir?

Bir sonraki derste görüşmek üzere! 👋