Ders 09: Form Yönetimi ve Validasyon
🎯 Bu Derste Neleri Öğreneceksiniz?
Section titled “🎯 Bu Derste Neleri Öğreneceksiniz?”- 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 Temelleri
Section titled “📚 Form Yönetimi Temelleri”Form yönetimi, kullanıcı girdilerini toplama, doğrulama ve gönderme işlemidir.
Temel Form Bileşenleri
Section titled “Temel Form Bileşenleri”| Bileşen | Açıklama |
|---|---|
| State | Form alanlarının değerleri |
| Validation | Girdilerin kurallara uygunluğu |
| Submission | Formun gönderilmesi |
| Errors | Validasyon hataları |
| Feedback | Kullanıcıya bilgi verme |
🎯 Controlled Components ile Form
Section titled “🎯 Controlled Components ile Form”Basit Form Örneği
Section titled “Basit Form Örneği”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} 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 ile Validasyon
Section titled “✅ Zod ile Validasyon”Zod, TypeScript-first bir validasyon kütüphanesidir.
Zod Şema Oluşturma
Section titled “Zod Şema Oluşturma”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 inferenceexport type IletisimForm = z.infer<typeof iletisimSchema>export type KayitForm = z.infer<typeof kayitSchema>export type UrunForm = z.infer<typeof urunSchema>Zod ile Validasyon Fonksiyonu
Section titled “Zod ile Validasyon Fonksiyonu”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 }}Form Validasyonu
Section titled “Form Validasyonu”// 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)}🔄 Server Functions ile Form
Section titled “🔄 Server Functions ile Form”Server-Side Validasyon
Section titled “Server-Side Validasyon”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: '[email protected]', ...data })
return { basari: true, mesaj: 'Mesajınız başarıyla gönderildi', } })Server Function Kullanımı
Section titled “Server Function Kullanımı”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> )}🎨 Custom Form Hook
Section titled “🎨 Custom Form Hook”Form yönetimi için kendi hook’unuzu oluşturun.
useForm Hook
Section titled “useForm Hook”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, }}Hook Kullanımı
Section titled “Hook Kullanımı”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> )}🎨 Pratik Örnek: Çok Adımlı Form
Section titled “🎨 Pratik Örnek: Çok Adımlı Form”Multi-step form (wizard), birden fazla adımdan oluşan formlardır.
Adım Yönetimi
Section titled “Adım Yönetimi”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> )}✅ Ders 9 Özeti
Section titled “✅ Ders 9 Özeti”Bu derste öğrendiklerimiz:
| Konu | Açıklama |
|---|---|
| Controlled components | Form alanları state ile yönetilir |
| Uncontrolled components | ref ile değer alınır |
| Zod | TypeScript-first validasyon |
| Server validation | Server function’lar ile validasyon |
| Form hooks | Özel hook’larla form yönetimi |
| Multi-step forms | Adım adım formlar |
| Error handling | Hata mesajları ve geri bildirim |
Validasyon Stratejileri
Section titled “Validasyon Stratejileri”| Strateji | Zaman | Avantaj |
|---|---|---|
| Client validation | Anında | Hızlı geri bildirim |
| Server validation | Submit’te | Güvenli, veri tutarlılığı |
| Hybrid | Her ikisi de | En iyi UX + güvenlik |
📝 Alıştırmalar
Section titled “📝 Alıştırmalar”Alıştırma 1: Ürün Formu
Section titled “Alıştırma 1: Ürün Formu”Ürün ekleme formu oluşturun:
// Başlık, açıklama, fiyat, stok, kategori// Resim yükleme// Zod validasyonuAlıştırma 2: Arama Formu
Section titled “Alıştırma 2: Arama Formu”Filtreleme ile arama formu:
// Anahtar kelime, kategori, fiyat aralığı// URL search params'a kaydet// Enter'a basınca araAlıştırma 3: Form Draft
Section titled “Alıştırma 3: Form Draft”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
💬 Sorularınız?
Section titled “💬 Sorularınız?”- Client vs server validasyon hangisi daha iyi?
- Form verisi nerede saklanmalı?
- Multi-step form nasıl test edilir?
Bir sonraki derste görüşmek üzere! 👋