İçeriğe geç
Sedat Demir
Geri dön

React 19 Form Actions ve useActionState: Server-Side Form Yönetiminin Yeni Çağı

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ı


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:

  1. previousState: Action fonksiyonunun ilk parametresi, bir önceki state değeridir. Bu sayede state'i kümülatif olarak güncelleyebilirsiniz.
  2. isPending: Loading spinner veya button disabled durumu için ekstra state'e gerek kalmaz.
  3. Return değeri: Action fonksiyonunun döndürdüğü nesne, otomatik olarak yeni state olur.

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: useFormStatus hook'u, <form> elementinin çocuk bileşenlerinde çalışır. Aynı bileşende <form> ve useFormStatus birlikte 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:

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ı.


Share this post on:

Sonraki Yazı
React Activity Bileşeni: State Kaybetmeden UI Parçalarını Gizle ve Göster