shadcn/ui ile TanStack Form kullanırken form durumu @tanstack/react-form paketindeki useForm kancasıyla yönetilir, her alan form.Field bileşeninin render fonksiyonu içinde çizilir ve görünüm shadcn/ui Field bileşenleriyle kurulur. Doğrulama için Zod şeması doğrudan validators seçeneğine verilir; ayrı bir resolver gerekmez.
Bu yazı resmî TanStack Form rehberini temel alır: şemayı, form örneğini, form.Field ile alan çizmeyi, hata gösterimini, Select, Checkbox, RadioGroup ve Switch bağlantılarını, mode="array" ile dizi alanlarını ve sıfırlamayı anlatıyoruz. Aynı formun React Hook Form sürümü React Hook Form yazısında; genel yol haritası shadcn/ui öğrenme rehberi sayfasında.
Yaklaşım
Belge TanStack Form yaklaşımını şu maddelerle özetler: form durumu için useForm, kontrollü girdiler için render prop kalıbıyla form.Field, erişilebilir düzen için Field, istemci tarafı doğrulama için Zod ve gerçek zamanlı doğrulama geri bildirimi. React Hook Form rehberinden temel farkı, kontrol bağlantılarının yayılım ({...field}) yerine açık özelliklerle yapılmasıdır: değer field.state.value, değişiklik field.handleChange, odak kaybı field.handleBlur ile bağlanır.
Kurulum tarafında shadcn/ui bileşenleri her zamanki gibi CLI ile eklenir. TanStack Form paketinin kurulumu shadcn/ui belgesinde ayrıca verilmez; kod @tanstack/react-form ve zod paketlerini import eder, paket kurulumu için TanStack Form belgesine bakın.
npx shadcn@latest add field input textarea buttonŞema ve form örneği
Şema React Hook Form rehberindekiyle aynıdır. Belge burada da zod v3 kullanıldığını ve TanStack Form ile Standard Schema uyumlu diğer kütüphanelerin validators API üzerinden çalıştığını not eder.
"use client"
import { useForm } from "@tanstack/react-form"
import { toast } from "sonner"
import * as z from "zod"
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 function HataBildirimFormu() {
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
toast.success("Form başarıyla gönderildi")
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
{/* alanlar burada */}
</form>
)
}Bu kodda üç nokta React Hook Form alışkanlıklarından ayrılır. Birincisi, gönderim işleyicisi useForm seçeneklerinin içinde tanımlanır ve değerleri { value } olarak alır. İkincisi, şema resolver ile sarılmadan doğrudan validators.onSubmit alanına verilir. Üçüncüsü, form öğesinin onSubmit olayında tarayıcının varsayılan gönderimi e.preventDefault() ile durdurulur ve ardından form.handleSubmit() parametresiz çağrılır. Bu satırı unutursanız sayfa yeniden yüklenir.
Resmî örneklerde useForm çağrısına ayrıca bir tür parametresi verilmez; değerlerin biçimi defaultValues nesnesiyle belirlenir. Onay kutusu örneği bu yüzden boş diziyi [] as string[] biçiminde açıkça türlendirir. Tür çıkarımı konusu için TypeScript rehberine bakabilirsiniz.
form.Field ile alan çizmek
Her alan bir form.Field ile tanımlanır. name alan adını, children ise alan nesnesini alan bir fonksiyonu tutar. Belgedeki anatomi örneğinin Türkçe metinli hali şöyledir:
<form.Field
name="title"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Hata başlığı</FieldLabel>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="Mobilde giriş düğmesi çalışmıyor"
autoComplete="off"
/>
<FieldDescription>Hatayı kısa bir başlıkla özetleyin.</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>En önemli satır isInvalid hesabıdır: alan hem dokunulmuş (isTouched) hem de geçersiz (!isValid) olduğunda hata gösterilir. Bu sayede kullanıcı alana hiç dokunmadan kırmızı uyarılarla karşılaşmaz. Hesaplanan değer, React Hook Form rehberindeki gibi üç yere gider: Field üzerindeki data-invalid, kontroldeki aria-invalid ve FieldError koşulu.
Hatalar field.state.meta.errors dizisinden gelir ve doğrudan FieldError bileşeninin errors özelliğine verilir; React Hook Form tarafındaki gibi diziye sarmaya gerek yoktur. Field belgesi FieldError bileşeninin Standard Schema hata kayıtlarını kabul ettiğini belirttiği için Zod mesajları olduğu gibi görünür. Birden fazla farklı mesaj varsa bileşen bunları liste olarak çizer (Field yazısı).
Resmî örnek dosyaların en üstünde /* eslint-disable react/no-children-prop */ satırı bulunur. Nedeni, children değerinin JSX çocuğu olarak değil bir özellik olarak verilmesidir ve bazı ESLint yapılandırmaları buna uyarı verir. Dizi örneğinde ise aynı fonksiyon <form.Field name="emails" mode="array">{(field) => ...}</form.Field> biçiminde JSX çocuğu olarak yazılmıştır; iki biçim de resmî örneklerde geçer.
Doğrulama modları

TanStack Form doğrulama zamanlamasını validators nesnesinin anahtarlarıyla belirler. Belgedeki tablo üç mod listeler ve aynı şemanın birden fazla moda verilebileceğini gösterir:
| Anahtar | Doğrulama ne zaman çalışır |
|---|---|
| onChange | Her değişiklikte |
| onBlur | Alan odağı kaybettiğinde |
| onSubmit | Gönderimde |
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
onChange: formSchema,
onBlur: formSchema,
},
})Belgenin kendi örnekleri yalnızca onSubmit kullanır ve diğer doğrulama stratejileri için TanStack belgesindeki dinamik doğrulama rehberine yönlendirir. isTouched koşulu sayesinde onChange açıkken bile kullanıcı yalnızca dokunduğu alanlarda hata görür.
Farklı kontrol türleri

| Kontrol | Değer | Değişiklik | aria-invalid nereye |
|---|---|---|---|
| Input, Textarea | value={field.state.value} | onChange içinde handleChange(e.target.value) | Input / Textarea |
| Select | value={field.state.value} | onValueChange={field.handleChange} | SelectTrigger |
| Checkbox dizisi | checked={field.state.value.includes(id)} | pushValue / removeValue | Checkbox |
| RadioGroup | value={field.state.value} | onValueChange={field.handleChange} | RadioGroupItem |
| Switch | checked={field.state.value} | onCheckedChange={field.handleChange} | Switch |
Input ve Textarea olay nesnesi verdiği için değer e.target.value ile çıkarılır. Radix tabanlı kontroller ise doğrudan değeri verir; bu yüzden field.handleChange fonksiyonu onlara olduğu gibi aktarılabilir. Switch örneği:
<form.Field
name="twoFactor"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="horizontal" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="iki-adim">İki adımlı doğrulama</FieldLabel>
<FieldDescription>Hesabınızı korumak için açın.</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldContent>
<Switch
id="iki-adim"
name={field.name}
checked={field.state.value}
onCheckedChange={field.handleChange}
aria-invalid={isInvalid}
/>
</Field>
)
}}
/>Resmî Switch örneğinin şeması z.boolean().refine((val) => val === true, ...) biçimindedir; yani alan yalnızca açık olduğunda geçerli sayılır. Bu, bir onay kutusunu ya da anahtarı zorunlu kılmanın basit bir yoludur.
Onay kutusu dizilerinde belge form.Field üzerinde mode="array" kullanılmasını ve TanStack Form dizi yardımcılarının çağrılmasını ister. İşaretlenen kutu field.pushValue(task.id) ile eklenir; kaldırılan kutunun indeksi indexOf ile bulunur ve field.removeValue(index) ile çıkarılır:
<Checkbox
id={`gorev-${task.id}`}
name={field.name}
aria-invalid={isInvalid}
checked={field.state.value.includes(task.id)}
onCheckedChange={(checked) => {
if (checked) {
field.pushValue(task.id)
} else {
const index = field.state.value.indexOf(task.id)
if (index > -1) {
field.removeValue(index)
}
}
}}
/>Liste yine data-slot="checkbox-group" taşıyan bir FieldGroup içine konur. Kontrollerin kendileri için Checkbox, Radio Group, Switch, Slider ve Select yazılarına bakabilirsiniz.
RadioGroup ve Select bağlantısı
Belgedeki RadioGroup örneği seçenekleri seçim kartı olarak çizer ve TanStack bağlantısını kök bileşende kurar: RadioGroup bileşenine name={field.name}, value={field.state.value} ve onValueChange={field.handleChange} verilir. Hata görünümü için aria-invalid her RadioGroupItem üzerine, data-invalid ise her kartın içindeki yatay Field bileşenine konur. Hata mesajı tek bir kez, FieldSet sonunda çizilir.
<RadioGroup
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
>
{plans.map((plan) => (
<FieldLabel key={plan.id} htmlFor={`plan-${plan.id}`}>
<Field orientation="horizontal" data-invalid={isInvalid}>
<FieldContent>
<FieldTitle>{plan.title}</FieldTitle>
<FieldDescription>{plan.description}</FieldDescription>
</FieldContent>
<RadioGroupItem value={plan.id} id={`plan-${plan.id}`} aria-invalid={isInvalid} />
</Field>
</FieldLabel>
))}
</RadioGroup>Select tarafında da aynı üçlü kullanılır: name, value ve onValueChange={field.handleChange} kök Select bileşenine gider, aria-invalid ve id ise SelectTrigger üzerine konur. Belgedeki örnek ayrıca SelectContent position="item-aligned" kullanır ve alanı orientation="responsive" ile kapsayıcı genişliğine göre yan yana ya da alt alta dizer. Radix kontrolleri olay nesnesi değil değer verdiği için handleChange fonksiyonunu sarmadan aktarmak bu kontrollerde güvenlidir.
Dizi alanları ve iç içe alanlar
Dinamik listeler için üst alan mode="array" ile tanımlanır ve field.state.value.map() ile her öğe için iç içe bir form.Field çizilir. Belge, dizi öğelerine köşeli parantez gösterimiyle erişildiğini belirtir: emails[${index}].address. React Hook Form tarafındaki nokta gösteriminden (emails.${index}.address) farklı olduğuna dikkat edin.
<form.Field name="emails" mode="array">
{(field) => (
<FieldSet className="gap-4">
<FieldLegend variant="label">E-posta adresleri</FieldLegend>
<FieldDescription>Size ulaşabileceğimiz en fazla 5 adres ekleyin.</FieldDescription>
<FieldGroup className="gap-4">
{field.state.value.map((_, index) => (
<form.Field
key={index}
name={`emails[${index}].address`}
children={(subField) => {
const isSubFieldInvalid =
subField.state.meta.isTouched && !subField.state.meta.isValid
return (
<Field orientation="horizontal" data-invalid={isSubFieldInvalid}>
<FieldContent>
<InputGroup>
<InputGroupInput
id={`eposta-${index}`}
name={subField.name}
value={subField.state.value}
onBlur={subField.handleBlur}
onChange={(e) => subField.handleChange(e.target.value)}
aria-invalid={isSubFieldInvalid}
placeholder="ad@ornek.com"
type="email"
/>
{field.state.value.length > 1 && (
<InputGroupAddon align="inline-end">
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
onClick={() => field.removeValue(index)}
aria-label={`E-posta ${index + 1} kaldır`}
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
)}
</InputGroup>
{isSubFieldInvalid && <FieldError errors={subField.state.meta.errors} />}
</FieldContent>
</Field>
)
}}
/>
))}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => field.pushValue({ address: "" })}
disabled={field.state.value.length >= 5}
>
E-posta ekle
</Button>
</FieldGroup>
</FieldSet>
)}
</form.Field>Ekleme field.pushValue({ address: "" }), kaldırma field.removeValue(index) ile yapılır; her ikisi de üst alan nesnesi üzerinden çağrılır, iç alan (subField) üzerinden değil. Resmî örnekte iç alanlar key={index} ile anahtarlanmıştır. Dizi doğrulaması React Hook Form rehberindeki Zod şemasıyla aynıdır: z.array(...).min(1, ...).max(5, ...). Dizi düzeyindeki hata (örneğin "en az bir adres ekleyin") üst alanın field.state.meta.errors değerinden okunur ve listenin altına çizilir.
Resmî örneklerdeki şemalar
TanStack Form galerisindeki her örnek, farklı bir Zod kalıbını da gösterir; kendi formunuzu yazarken bunları başlangıç noktası olarak kullanabilirsiniz. Giriş örneğinde kullanıcı adı en az 3, en fazla 10 karakterle sınırlanır ve regex(/^[a-zA-Z0-9_]+$/, ...) ile yalnızca harf, rakam ve alt çizgiye izin verilir. Radyo grubu örneğinde plan alanı z.string().min(1, ...) ile zorunlu hale getirilir; boş metin seçilmemiş anlamına gelir.
Onay kutusu örneğinde tasks alanı z.array(z.string()).min(1, ...) ile en az bir seçim ister ve ardından refine() ile seçilen her kimliğin bilinen görev listesinde olup olmadığını denetler. Switch örneği daha önce gördüğümüz gibi z.boolean().refine((val) => val === true, ...) kullanır. Bu örneklerin hepsinde şema yalnızca validators.onSubmit alanına verilir ve mesajlar şemanın içinde yazılır. Böylece hata metinlerini bileşenlere dağıtmadan tek bir yerde, örneğin Türkçe olarak, yönetebilirsiniz.
Resmî demo nasıl kurulmuş?
TanStack demosu, React Hook Form demosuyla aynı kart yapısını kullanır: başlık, id="bug-report-form" taşıyan form ve altbilgide düğmeler. Açıklama alanı bir InputGroupTextarea ile yazılır ve altındaki sayaç field.state.value.length değerini okuyarak /100 characters biçiminde güncel uzunluğu gösterir. Bu, render fonksiyonunun alan durumuna doğrudan eriştiğini gösteren güzel bir örnektir: sayaç için ayrı bir React durumu tutmanız gerekmez.
Gönderim işleyicisi useForm seçeneklerindeki onSubmit içinde çalışır ve gelen value nesnesini Sonner toast() ile JSON olarak gösterir. Resmî galeride ayrıca giriş, metin alanı, Select, onay kutusu, radyo, Switch, dizi ve hepsini birleştiren karmaşık bir form örneği vardır; karmaşık örnek onay kutusu listesini yine mode="array" ile yönetir.
Sıfırlama
Belge, formu varsayılan değerlere döndürmek için form.reset() çağrısını gösterir. Resmî demoda Sıfırla ve Gönder düğmeleri kart altbilgisinde, formun dışında durur; Gönder düğmesi bu yüzden form="bug-report-form" özniteliğiyle forma bağlanır, Sıfırla düğmesi ise type="button" olarak kalır.
<Button type="button" variant="outline" onClick={() => form.reset()}>
Sıfırla
</Button>React Hook Form ile karşılaştırma

İki resmî rehber aynı formu kurduğu için farklar net görünür. Aşağıdaki tablo yalnızca belgelerde ve örnek kodlarda görülen farkları özetler:
| Konu | React Hook Form rehberi | TanStack Form rehberi |
|---|---|---|
| Şema bağlantısı | resolver: zodResolver(formSchema) | validators: { onSubmit: formSchema } |
| Alan bileşeni | Controller ve render | form.Field ve children |
| Kontrol bağlantısı | {...field} yayılımı | value, onChange, onBlur açıkça |
| Hata koşulu | fieldState.invalid | isTouched && !isValid |
| FieldError errors | [fieldState.error] | field.state.meta.errors |
| Gönderim | form.handleSubmit(onSubmit) | preventDefault, sonra form.handleSubmit() |
| Dizi alanları | useFieldArray, append, remove | mode="array", pushValue, removeValue |
| Dizi alan adı | emails.0.address | emails[0].address |
Belge hangisinin daha iyi olduğu konusunda bir yargı vermez; ikisini de Field bileşeniyle eşit düzeyde destekler. Seçimi ekibinizin alışkanlığına ve projenin geri kalanına göre yapın. Sunucu tarafında çalışan üçüncü bir yol için Server Actions yazısına bakın.
Sık yapılan hatalar
e.preventDefault()çağırmamak: tarayıcı formu klasik yolla gönderir ve sayfa yenilenir.- Input üzerinde
onChange={field.handleChange}yazmak: Input olay nesnesi verir, değerie.target.valueile çıkarın. Bu kısa yazım yalnızca değer veren Radix kontrollerinde geçerlidir. onBlur={field.handleBlur}bağlamayı atlamak: resmî örneklerin tamamında metin kontrolleri bu bağlantıyla yazılır veonBlurdoğrulama modu odak kaybı olayına dayanır.- Hataları diziye sarmak: TanStack tarafında
field.state.meta.errorszaten bir dizidir. - Dizi alan adını nokta gösterimiyle yazmak: belge
emails[${index}].addressbiçimini kullanır. - Boş diziyi türlendirmemek: resmî onay kutusu örneği varsayılan değeri
[] as string[]olarak yazar; aynısını yapın.
Sık Sorulan Sorular
Zod dışında bir kütüphane kullanabilir miyim?
Belge, TanStack Form ile Zod ve Standard Schema uyumlu diğer kütüphanelerin validators API üzerinden çalıştığını not eder. Örnekler zod v3 ile yazılmıştır.
Gönderim sürerken düğmeyi nasıl devre dışı bırakırım?
shadcn/ui TanStack Form rehberi bekleme durumunu ele almaz. Bekleme durumunu data-disabled ve Spinner ile gösteren resmî örnek Next.js Server Actions rehberindedir (Server Actions yazısı); TanStack tarafındaki durum alanları için TanStack belgesine bakın.
Combobox ya da OTP alanını form.Field ile bağlayabilir miyim?
Evet, kalıp aynıdır: değeri field.state.value ile okuyup seçimde field.handleChange çağırırsınız. Bileşenlerin kendileri Combobox ve Input OTP yazılarında.
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 · React Hook Form ve Zod · Field bileşeni · Next.js Server Actions formu · Input Group · Seçim kontrolleri