React 19 Form Actions ve useActionState: Server-Side Form Yönetiminin Yeni Çağı
React 19, frontend geliştirme dünyasında sessiz bir devrim yaratıyor. Bu devrimin en dikkat çekici parçalarından biri, form yönetimi konusundaki radikal değişiklikler. Yıllardır useState, onChange, onSubmit üçlüsüyle boğuştuğumuz form işlemleri artık çok daha temiz, çok daha deklaratif ve çok daha güçlü bir yapıya kavuşuyor.
Bu yazıda Form Actions ve useActionState hook'unu derinlemesine inceleyeceğiz, gerçek dünya senaryolarıyla bu yeni API'ların nasıl kullanıldığını adım adım göreceğiz.
Geleneksel Form Yönetimindeki Sorunlar
React'te form yönetimi her zaman "verbose" (uzun soluklu) bir iş olmuştur. Klasik bir login formu düşünelim:
// ❌ Eski yöntem - React 18 ve öncesi
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
setError(null);
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const data = await response.json();
if (!response.ok) throw new Error(data.message);
// Başarılı giriş işlemi
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<input value={password} onChange={(e) => setPassword(e.target.value)} />
{error && <p>{error}</p>}
<button disabled={isLoading}>
{isLoading ? 'Giriş yapılıyor...' : 'Giriş Yap'}
</button>
</form>
);
}Bu kodda dört adet state, bir async handler, manuel hata yönetimi ve loading kontrolü var. Sadece iki alanlı basit bir form için bile bu kadar boilerplate kod yazıyoruz. Formunuzda 10 alan olduğunu düşünün — kabus.
Form Actions Nedir?
React 19, <form> elementinin action prop'una doğrudan bir fonksiyon geçmenize olanak tanıyor. Bu fonksiyon, formun FormData nesnesini parametre olarak alır ve senkron veya asenkron olarak çalışabilir.
// ✅ React 19 - Form Actions
function LoginForm() {
async function login(formData) {
const email = formData.get('email');
const password = formData.get('password');
await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
}
return (
<form action={login}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Giriş Yap</button>
</form>
);
}Fark ettiniz mi? e.preventDefault() yok. onChange handler'ları yok. useState yok. Form, native HTML davranışına çok daha yakın bir şekilde çalışıyor.
Form Actions'ın Temel Avantajları
- Sıfır state yönetimi: Form verileri
FormDataAPI'si üzerinden okunur - Otomatik
preventDefault: React bunu sizin yerinize halleder - Progressive Enhancement: JavaScript yüklenmeden önce bile form çalışabilir
- Server Actions desteği: Next.js gibi framework'lerle doğrudan sunucu fonksiyonlarına bağlanabilir
useActionState Hook'u: Formun Beyni
Form Actions tek başına güçlü olsa da, gerçek dünyada loading durumu, hata mesajları ve önceki state gibi bilgilere ihtiyaç duyarız. İşte useActionState tam olarak bu boşluğu dolduruyor.
Temel Sözdizimi
const [state, formAction, isPending] = useActionState(actionFn, initialState);| Parametre | Açıklama |
|---|---|
actionFn |
(previousState, formData) => newState şeklinde bir fonksiyon |
initialState |
State'in başlangıç değeri |
state |
Action'ın döndürdüğü güncel state |
formAction |
<form action> prop'una geçilecek fonksiyon |
isPending |
Action çalışırken true olan boolean değer |
İlk Örnek: Basit Bir İletişim Formu
'use client';
import { useActionState } from 'react';
async function submitContact(previousState, formData) {
const name = formData.get('name');
const email = formData.get('email');
const message = formData.get('message');
// Validasyon
if (!name || name.length < 2) {
return { success: false, error: 'İsim en az 2 karakter olmalıdır.' };
}
if (!email || !email.includes('@')) {
return { success: false, error: 'Geçerli bir email adresi giriniz.' };
}
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, message }),
});
if (!response.ok) {
return { success: false, error: 'Bir hata oluştu. Tekrar deneyin.' };
}
return { success: true, error: null };
} catch {
return { success: false, error: 'Sunucuya bağlanılamadı.' };
}
}
function ContactForm() {
const [state, formAction, isPending] = useActionState(submitContact, {
success: false,
error: null,
});
return (
<form action={formAction}>
<div>
<label htmlFor="name">İsim</label>
<input id="name" name="name" type="text" required />
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
</div>
<div>
<label htmlFor="message">Mesaj</label>
<textarea id="message" name="message" rows={4} />
</div>
{state.error && (
<div className="error-message">{state.error}</div>
)}
{state.success && (
<div className="success-message">
Mesajınız başarıyla gönderildi!
</div>
)}
<button type="submit" disabled={isPending}>
{isPending ? 'Gönderiliyor...' : 'Gönder'}
</button>
</form>
);
}Bu örnekte dikkat edilmesi gereken kritik noktalar:
previousState: Action fonksiyonunun ilk parametresi, bir önceki state değeridir. Bu sayede state'i kümülatif olarak güncelleyebilirsiniz.isPending: Loading spinner veya button disabled durumu için ekstra state'e gerek kalmaz.- Return değeri: Action fonksiyonunun döndürdüğü nesne, otomatik olarak yeni
stateolur.
Server Actions ile Entegrasyon (Next.js)
React 19'un Form Actions özelliği, Next.js App Router ile birleştiğinde gerçek gücünü gösterir. Server Actions sayesinde form verileriniz doğrudan sunucu tarafında çalışan fonksiyonlara ulaşır — arada API route yazmaya gerek kalmaz.
Server Action Dosyası
// app/actions/auth.ts
'use server';
import { z } from 'zod';
import { db } from '@/lib/database';
import { redirect } from 'next/navigation';
const registerSchema = z.object({
name: z.string().min(2, 'İsim en az 2 karakter olmalı'),
email: z.string().email('Geçerli bir email giriniz'),
password: z.string().min(8, 'Şifre en az 8 karakter olmalı'),
});
export async function registerUser(previousState, formData) {
const rawData = {
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
};
// Zod ile validasyon
const validated = registerSchema.safeParse(rawData);
if (!validated.success) {
return {
success: false,
errors: validated.error.flatten().fieldErrors,
message: null,
};
}
// Veritabanı kontrolü
const existingUser = await db.user.findUnique({
where: { email: validated.data.email },
});
if (existingUser) {
return {
success: false,
errors: { email: ['Bu email adresi zaten kayıtlı'] },
message: null,
};
}
// Kullanıcı oluşturma
await db.user.create({
data: validated.data,
});
redirect('/dashboard');
}Client Component
// app/register/page.tsx
'use client';
import { useActionState } from 'react';
import { registerUser } from '@/app/actions/auth';
export default function RegisterPage() {
const [state, formAction, isPending] = useActionState(registerUser, {
success: false,
errors: {},
message: null,
});
return (
<div className="max-w-md mx-auto mt-10">
<h1 className="text-2xl font-bold mb-6">Kayıt Ol</h1>
<form action={formAction} className="space-y-4">
<div>
<label htmlFor="name">Ad Soyad</label>
<input
id="name"
name="name"
type="text"
className="w-full border rounded p-2"
/>
{state.errors?.name && (
<p className="text-red-500 text-sm mt-1">
{state.errors.name[0]}
</p>
)}
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
className="w-full border rounded p-2"
/>
{state.errors?.email && (
<p className="text-red-500 text-sm mt-1">
{state.errors.email[0]}
</p>
)}
</div>
<div>
<label htmlFor="password">Şifre</label>
<input
id="password"
name="password"
type="password"
className="w-full border rounded p-2"
/>
{state.errors?.password && (
<p className="text-red-500 text-sm mt-1">
{state.errors.password[0]}
</p>
)}
</div>
<button
type="submit"
disabled={isPending}
className="w-full bg-blue-600 text-white py-2 rounded
disabled:opacity-50 disabled:cursor-not-allowed"
>
{isPending ? 'Kayıt yapılıyor...' : 'Kayıt Ol'}
</button>
</form>
</div>
);
}Bu yapıda form verisi doğrudan sunucuya gider, veritabanı işlemi sunucuda gerçekleşir ve sonuç client'a state olarak döner. Arada hiçbir API route yazmadık.
useFormStatus ile Çocuk Bileşenlerde Pending Durumu
useActionState'in isPending değeri yalnızca formu tanımlayan bileşende erişilebilir. Eğer submit butonunu ayrı bir bileşen olarak tasarladıysanız, useFormStatus hook'unu kullanabilirsiniz:
import { useFormStatus } from 'react-dom';
function SubmitButton({ children }) {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
className={`btn ${pending ? 'btn-loading' : 'btn-primary'}`}
>
{pending ? (
<span className="flex items-center gap-2">
<Spinner size="sm" />
İşleniyor...
</span>
) : (
children
)}
</button>
);
}
// Kullanımı
<form action={formAction}>
{/* ... input alanları ... */}
<SubmitButton>Kaydet</SubmitButton>
</form>Önemli:
useFormStatushook'u,<form>elementinin çocuk bileşenlerinde çalışır. Aynı bileşende<form>veuseFormStatusbirlikte kullanılamaz.
Optimistic Updates ile useOptimistic
React 19 ayrıca useOptimistic hook'unu da beraberinde getiriyor. Form Actions ile birleştiğinde, kullanıcıya anında geri bildirim verebilirsiniz:
import { useOptimistic, useActionState } from 'react';
function TodoList({ todos }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(currentTodos, newTodo) => [
...currentTodos,
{ id: crypto.randomUUID(), text: newTodo, pending: true },
]
);
async function addTodo(previousState, formData) {
const text = formData.get('todo');
addOptimisticTodo(text);
// Sunucu isteği
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text }),
});
return await response.json();
}
const [state, formAction] = useActionState(addTodo, null);
return (
<div>
<form action={formAction}>
<input name="todo" placeholder="Yeni görev..." />
<button type="submit">Ekle</button>
</form>
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.6 : 1 }}>
{todo.text}
{todo.pending && ' (kaydediliyor...)'}
</li>
))}
</ul>
</div>
);
}Kullanıcı formu gönderdiğinde, yeni todo anında listede görünür (soluk renkte). Sunucu yanıt verdiğinde opacity normale döner. Hata olursa React optimistic güncellemeyi otomatik olarak geri alır.
En İyi Pratikler ve İpuçları
1. State Yapınızı İyi Tasarlayın
// ✅ İyi bir action state yapısı
const initialState = {
success: false,
errors: {}, // Alan bazlı hatalar
message: null, // Genel mesaj
data: null, // Dönen veri
};2. Validasyonu Her Zaman Sunucu Tarafında da Yapın
Client-side validasyon kullanıcı deneyimi içindir, güvenlik için değil. Server Action'larda mutlaka Zod veya benzeri bir kütüphane ile validasyon yapın.
3. Form Reset İşlemi
Başarılı bir submit sonrası formu sıfırlamak için useRef kullanabilirsiniz:
function MyForm() {
const formRef = useRef(null);
async function action(prevState, formData) {
const result = await saveData(formData);
if (result.success) {
formRef.current?.reset();
}
return result;
}
const [state, formAction] = useActionState(action, { success: false });
return <form ref={formRef} action={formAction}>{/* ... */}</form>;
}4. useActionState vs useFormState
Eğer daha önce React Canary sürümlerinde useFormState kullandıysanız, useActionState'in onun yeniden adlandırılmış ve geliştirilmiş hali olduğunu bilin. useActionState ek olarak isPending döndürür.
Sonuç
React 19'un Form Actions ve useActionState hook'u, form yönetimini temelden değiştiriyor. Yıllardır yazdığımız boilerplate kodların büyük bir kısmı artık tarih oluyor.
Özetle bu yeni API'lar ile:
useStateveonChangehandler'larından kurtuluyorsunuz- Loading ve error state'lerini tek bir hook ile yönetiyorsunuz
- Server Actions ile API route katmanını ortadan kaldırıyorsunuz
useOptimisticile anlık kullanıcı geri bildirimi sağlıyorsunuz- Progressive enhancement desteği sayesinde erişilebilirlik artıyor
Bu değişiklikler sadece kod miktarını azaltmakla kalmıyor, aynı zamanda mental modeli de basitleştiriyor. Form artık bir "state makinesi" değil, bir "action dispatcher" — ve bu paradigma kayması, React'in geleceğini şekillendiren en önemli adımlardan biri.
Yeni projelerde bu API'ları hemen denemeye başlamanızı, mevcut projelerde ise geçiş planı oluşturmanızı şiddetle öneriyorum. Formlarınız hiç bu kadar temiz olmamıştı.