shadcn/ui ile Next.js Server Actions formu kurmak için form next/form paketindeki Form bileşeniyle yazılır, durum React useActionState kancasıyla tutulur ve doğrulama sunucuda çalışan bir "use server" fonksiyonunda Zod safeParse() ile yapılır. Sunucu eylemi hataları ve kullanıcının girdiği değerleri geri döndürür; istemci bileşeni de bunları shadcn/ui Field ve FieldError bileşenleriyle gösterir.

Bu yazı shadcn/ui deposundaki Next.js form rehberini adım adım anlatır: şema ve durum türü, sunucu eylemi, form bileşeni, bekleme (pending) ve devre dışı durumları, iş kuralı doğrulaması, sıfırlama davranışı ve onay kutusu, radyo, Select ile Switch içeren karmaşık form. Server Actions ve App Router kavramlarının kendisi bu yazının konusu değil; onlar için Next.js App Router yazısına bakın. Seri genel bakışı için shadcn/ui öğrenme rehberi.

Yaklaşım

Belge yaklaşımı şu maddelerle anlatır: gezinme ve aşamalı geliştirme (progressive enhancement) için Next.js <Form /> bileşeni, erişilebilir düzen için <Field />, form durumu ve hatalar için useActionState, yükleniyor durumları için pending değeri, gönderimi işlemek için Server Actions ve sunucu tarafı doğrulama için Zod. React Hook Form ve TanStack Form rehberlerinden farkı, ortada istemci tarafı bir form kütüphanesi olmamasıdır: değerler tarayıcının yerel FormData nesnesiyle sunucuya gider ve alanlar kontrolsüz (defaultValue ile) yazılır.

Gerekli bileşenler Field, Input, Button ve bekleme göstergesi için Spinner bileşenidir. Resmî demoda açıklama sayacı için Input Group, başarı mesajı için Sonner, kap için Card de kullanılır.

bash
npx shadcn@latest add field input button spinner

1. Adım: Şema ve FormState türü

Şema ve form durumu türü ayrı bir schema.ts dosyasına yazılır. Belge bunun nedenini açıkça vurgular: aynı dosya hem istemci bileşenine hem de sunucu eylemine import edilecektir. Resmî örnek dosyasındaki yapıyı Türkçe mesajlarla yazalım:

typescript
import { z } from "zod"

export const formSchema = z.object({
  title: z
    .string()
    .min(5, "Başlık en az 5 karakter olmalı.")
    .max(32, "Başlık en fazla 32 karakter olabilir."),
  description: z
    .string()
    .min(20, "Açıklama en az 20 karakter olmalı.")
    .max(100, "Açıklama en fazla 100 karakter olabilir."),
})

export type FormState = {
  values: z.infer<typeof formSchema>
  errors: null | Partial<Record<keyof z.infer<typeof formSchema>, string[]>>
  success: boolean
}

FormState üç alandan oluşur. values, kullanıcının son gönderdiği değerleri tutar ve hata durumunda alanların yeniden doldurulmasını sağlar. errors, ya null ya da her alan adı için bir mesaj dizisi taşıyan bir nesnedir; dizi olmasının nedeni Zod flatten().fieldErrors çıktısının bu biçimde olmasıdır. success ise eylemin başarılı olup olmadığını bildirir. Belgenin metin içindeki tür tanımında values? isteğe bağlı yazılmıştır, depodaki örnek dosyada ise zorunludur; örnek dosya ilk durumda values verdiği için zorunlu sürüm daha tutarlıdır. Belge, şema kütüphanesini değiştirebileceğinizi ama Standard Schema belirtimine uyması gerektiğini not eder.

Dosya yapısı

Dosya yapısı: schema.ts, actions.ts, form.tsx

Rehber üç dosyalık bir düzen önerir ve bu düzen, istemci ile sunucu sınırını açıkça ayırdığı için önemlidir. Aşağıdaki liste her dosyanın görevini özetler:

  • schema.ts: Zod şeması ve FormState türü. Yönerge içermez; hem istemci hem sunucu tarafından import edilir.
  • actions.ts: "use server" ile başlar, FormData okur, doğrular ve yeni durumu döndürür.
  • form.tsx: "use client" ile başlar, useActionState kullanır ve Field bileşenleriyle arayüzü çizer.

Resmî örnekte useActionState çağrısına tür parametreleri açıkça verilir: React.useActionState<FormState, FormData>(...). İlk parametre durumun, ikincisi eyleme gönderilen verinin türüdür. Böylece formState.errors?.title gibi erişimler, şemadan türetilen FormState türüne göre denetlenir.

2. Adım: Sunucu eylemi

2. Adım: Sunucu eylemi

Sunucu eylemi, sunucuda çalışan ve istemciden çağrılabilen bir fonksiyondur. useActionState ile kullanıldığı için ilk parametresi önceki durum, ikincisi FormData olur. Aşağıdaki kod resmî actions.ts örneğinin yapısını korur:

typescript
"use server"

import { formSchema, type FormState } from "./schema"

export async function demoFormAction(
  _prevState: FormState,
  formData: FormData
) {
  const values = {
    title: formData.get("title") as string,
    description: formData.get("description") as string,
  }

  const result = formSchema.safeParse(values)

  if (!result.success) {
    return {
      values,
      success: false,
      errors: result.error.flatten().fieldErrors,
    }
  }

  // Değerlerle bir şey yapın: veritabanı ya da API çağrısı.

  return {
    values: {
      title: "",
      description: "",
    },
    errors: null,
    success: true,
  }
}

Değerler formData.get() ile alan adına göre okunur; bu adlar formdaki input öğelerinin name öznitelikleridir. safeParse() hata fırlatmaz, bir sonuç nesnesi döndürür; başarısızsa result.error.flatten().fieldErrors alan adına göre gruplanmış mesajları verir. Belge, hata durumunda values döndürülmesinin bilinçli olduğunu açıklar: kullanıcının yazdıkları form durumunda kalsın diye. Başarı durumunda ise boş değerler döndürülerek form temizlenir.

3. Adım: useActionState ile form bileşeni

typescript
"use client"

import * as React from "react"
import Form from "next/form"

import { Button } from "@/components/ui/button"
import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Spinner } from "@/components/ui/spinner"

import { demoFormAction } from "./actions"
import { type FormState } from "./schema"

export function HataBildirimFormu() {
  const [formState, formAction, pending] = React.useActionState<
    FormState,
    FormData
  >(demoFormAction, {
    values: {
      title: "",
      description: "",
    },
    errors: null,
    success: false,
  })

  return (
    <Form action={formAction}>
      <FieldGroup>
        <Field data-invalid={!!formState.errors?.title?.length}>
          <FieldLabel htmlFor="title">Hata başlığı</FieldLabel>
          <Input
            id="title"
            name="title"
            defaultValue={formState.values.title}
            disabled={pending}
            aria-invalid={!!formState.errors?.title?.length}
            placeholder="Mobilde giriş düğmesi çalışmıyor"
            autoComplete="off"
          />
          <FieldDescription>Hatayı kısa bir başlıkla özetleyin.</FieldDescription>
          {formState.errors?.title && (
            <FieldError>{formState.errors.title[0]}</FieldError>
          )}
        </Field>
        <Button type="submit" disabled={pending}>
          {pending && <Spinner />}
          Gönder
        </Button>
      </FieldGroup>
    </Form>
  )
}

useActionState üç değer döndürür: güncel form durumu, forma verilecek eylem ve bekleme durumu. İkinci argüman ilk durumdur; burada boş değerler, null hata ve false başarı verilir. <Form action={formAction}> ifadesi gönderimi sunucu eylemine bağlar. Input bileşenlerinde value değil defaultValue kullanılır; alanlar kontrolsüzdür ve her yanıtta formState.values içinden yeniden doldurulur.

Hata gösterimi diğer rehberlerle aynı üçlüye dayanır, ama koşul farklı yazılır: !!formState.errors?.title?.length ifadesi alan için en az bir mesaj olup olmadığını boolean olarak verir. Bu değer Field üzerindeki data-invalid ve Input üzerindeki aria-invalid özelliklerine gider. FieldError burada errors dizisiyle değil, ilk mesaj çocuk olarak verilerek kullanılır: formState.errors.title[0]. Field bileşeninin bu iki kullanımı Field yazısında karşılaştırılıyor.

Resmî demoda bu iskelete iki ek vardır. Birincisi, formState.success değiştiğinde bir useEffect içinde Sonner toast() çağrılarak teşekkür mesajı gösterilir (Sonner yazısı). İkincisi, açıklama alanının karakter sayacı ayrı bir descriptionLength durumunda tutulur; alan kontrolsüz olduğu için sayaç onChange içinde güncellenir ve sunucudan yeni değer geldiğinde bir efektle eşitlenir.

Bekleme ve devre dışı durumları

Belge pending değerini iki yerde kullanmayı gösterir. Gönder düğmesinde disabled={pending} ile çift gönderim engellenir ve {pending && <Spinner />} ile düğmenin içinde bir yükleniyor göstergesi çizilir. Alan düzeyinde ise kontrole disabled={pending}, çevreleyen Field bileşenine data-disabled={pending} verilir. Field kaynağında etiketin bu öznitelikle soluklaştığı tanımlıdır, böylece alanın tamamı devre dışı görünür.

typescript
<Field data-disabled={pending}>
  <FieldLabel htmlFor="name">Ad</FieldLabel>
  <Input id="name" name="name" disabled={pending} />
</Field>
<Field>
  <Button type="submit" disabled={pending}>
    {pending && <Spinner />} Gönder
  </Button>
</Field>

Spinner bileşeni Ekim 2025 bileşenleri arasında gelir; varyantları ve diğer yükleniyor göstergeleri Alert, Badge, Progress, Skeleton, Spinner yazısında anlatılıyor. Düğmeye yükleniyor durumu ekleme konusu Button yazısında da geçer.

İş kuralı doğrulaması

Şema yalnızca biçimi denetler. "Bu e-posta zaten kayıtlı mı?" gibi kurallar için sunucu eyleminin içinde ek kontrol yapılır ve aynı errors biçiminde yanıt döndürülür. Belgedeki örnek, şema geçtikten sonra veritabanında kullanıcı arar:

typescript
const existingUser = await db.user.findUnique({
  where: { email: result.data.email },
})

if (existingUser) {
  return {
    values,
    success: false,
    errors: {
      email: ["Bu e-posta adresi zaten kayıtlı"],
    },
  }
}

Buradaki db nesnesi örnekte tanımlanmamış bir yer tutucudur; kendi veritabanı katmanınızla değiştirirsiniz. Belgenin bu örneği şemada email alanı olduğunu varsayar, oysa aynı sayfadaki demo şemasında yalnızca title ve description vardır; kendi şemanıza uyarlarken alan adlarını eşleştirin. Belge burada da hata durumunda values döndürmeyi hatırlatır.

Sıfırlama davranışı

Sıfırlama davranışı: Başarıda sıfırlamak, Hatada korumak

Belge, sunucu eylemiyle gönderilen bir formda React'in form durumunu otomatik olarak ilk değerlere sıfırladığını ve bunun standart React davranışı olduğunu söyler. Buradan iki kural çıkar:

  • Başarıda sıfırlamak: sunucu eyleminden values döndürmeyin; belgeye göre form ilk değerlerine döner. Depodaki demo eylemi ise başarıda boş değerler döndürerek aynı sonuca ulaşır.
  • Hatada korumak: doğrulama hatasında values döndürün. Alanlar defaultValue={formState.values.title} ile bu değerlerden doldurulduğu için kullanıcının yazdıkları kaybolmaz.

Bu iki kural birlikte düşünülmelidir. Hata durumunda değerleri döndürmeyi unutursanız kullanıcı her doğrulama hatasında formu baştan doldurmak zorunda kalır. Karmaşık form örneğinin eylemi ise başarıda da values döndürür; çünkü orada amaç bir tercih formunu kaydettikten sonra seçimleri ekranda tutmaktır. Hangisini seçeceğiniz formun amacına bağlıdır.

Karmaşık form: radyo, Select, onay kutusu ve Switch

Belgenin karmaşık örneği bir abonelik tercih formudur ve yerel FormData ile farklı kontrollerin nasıl okunacağını gösterir. Sunucu eylemi değerleri şöyle toplar:

typescript
const values = {
  plan: formData.get("plan") as FormState["values"]["plan"],
  billingPeriod: formData.get("billingPeriod") as string,
  addons: formData.getAll("addons") as string[],
  emailNotifications: formData.get("emailNotifications") === "on",
}

Burada üç ayrıntı öğreticidir. Birden fazla onay kutusu aynı name="addons" ile yazılır ve her biri kendi value değerini taşır; sunucuda formData.getAll() işaretli olanların dizisini verir. Switch için gönderilen değer "on" ile karşılaştırılarak boolean değere çevrilir. RadioGroup ve Select bileşenlerine ise name verilir (name="plan", name="billingPeriod") ve seçilen değer formData.get() ile okunur. İstemci tarafında bu kontroller defaultValue ya da defaultChecked ile önceki değerlerden başlatılır:

typescript
<Checkbox
  id={addon.id}
  name="addons"
  value={addon.id}
  defaultChecked={formState.values.addons.includes(addon.id)}
  disabled={pending}
  aria-invalid={!!formState.errors?.addons?.length}
/>

Karmaşık örneğin şeması z.array(z.string()).min(1, ...).max(3, ...) ile en az bir, en fazla üç eklenti seçilmesini ve refine() ile seçilen eklentilerin geçerli listede olmasını ister. Eylemin başında belgedeki örnek, bekleme durumunu görünür kılmak için bir saniyelik yapay bir bekleme ekler; bu yalnızca gösterim içindir. Kontrollerin kendileri seçim kontrolleri ve Select yazılarında anlatılıyor.

İstemci kütüphaneli rehberlerle karşılaştırma

KonuServer Actions rehberiReact Hook Form / TanStack Form
Doğrulama yeriSunucu (safeParse)İstemci (şema)
Alan türüKontrolsüz, defaultValueKontrollü, value
Durum kaynağıuseActionStateuseForm
Hata biçimiAlan adına göre string dizisiHata nesneleri
FieldError kullanımıİlk mesaj çocuk olarakerrors dizisi
Bekleme durumupending değeriRehberlerde ele alınmıyor

Sunucu tarafı doğrulama, istemci tarafı doğrulamanın yerine değil yanına konacak bir katman olarak da düşünülebilir; ancak shadcn/ui belgesi iki yaklaşımı birleştiren bir örnek vermez. İstemci tarafı seçenekler için React Hook Form ve TanStack Form yazılarına bakın. Uygulamayı kendi sunucunuzda yayınlamak için Next.js VPS yayınlama yazısı işinize yarayabilir.

Sık yapılan hatalar

  • Input üzerinde name unutmak: formData.get("title") yalnızca name özniteliği olan alanları okur.
  • Kontrolsüz alanda value kullanmak: örnekler defaultValue kullanır; value verirseniz alanı React durumuyla ayrıca yönetmeniz gerekir.
  • Hata yanıtında values döndürmemek: kullanıcının yazdıkları kaybolur.
  • Şemayı sunucu eylemi dosyasında tanımlamak: belge, istemci ve sunucunun aynı türü kullanabilmesi için şemayı ayrı schema.ts dosyasına koyar.
  • "use server" ve "use client" yönergelerini karıştırmak: eylem dosyası sunucu, form bileşeni istemci yönergesiyle başlar.
  • Switch değerini doğrudan boolean sanmak: FormData içinde metin gelir, örnekte "on" ile karşılaştırılır.

Sık Sorulan Sorular

next/form yerine düz form öğesi kullanabilir miyim?

Belgenin bütün örnekleri Next.js Form bileşenini kullanır ve onu gezinme ile aşamalı geliştirme için tercih eder. Düz form öğesiyle davranışın farklarını Next.js belgesinden kontrol etmeniz gerekir; shadcn/ui rehberi bu karşılaştırmayı yapmaz.

flatten() çıktısı neden dizi?

Bir alan birden fazla kuralı aynı anda bozabileceği için Zod her alan için bir mesaj dizisi üretir. Örnekler yalnızca ilk mesajı ([0]) gösterir.

Zod v4 ile aynı kod çalışır mı?

Belge örneklerin zod v3 ile yazıldığını belirtir. Başka sürüm ya da kütüphane kullanırsanız hata çıktısının biçimini o kütüphanenin belgesinden doğrulayın.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: shadcn/ui öğrenme rehberi · Field bileşeni · React Hook Form ve Zod · TanStack Form · shadcn/ui Next.js kurulumu · Next.js App Router