07 / 09
Dinamik alanlar ve çok adımlı formlar
useFieldArray ile eklenip çıkarılan satırlar, dizi düzeyindeki hata, Controller ile controlled bileşenler, FormProvider, adım adım doğrulama, geri dönünce değerleri korumak ve Next.js'teki Suspense notu.
useFieldArray
Bir satıcı başvurusunda "örnek ürünler" listesi düşünün: kullanıcı satır ekler, siler; her satırın adı ve fiyatı var. Şemada bu bir nesne dizisidir:1
const productSchema = z.object({
name: z.string().trim().min(2, "Ürün adı en az 2 karakter olmalı."),
price: z
.string()
.trim()
.min(1, "Fiyat girin.")
.regex(/^\d+([.,]\d{1,2})?$/, "Fiyatı 249,90 biçiminde girin."),
});
const storeSchema = z.object({
storeName: z.string().trim().min(2, "Mağaza adı en az 2 karakter olmalı."),
products: z
.array(productSchema)
.min(1, "En az bir ürün ekleyin.")
.max(3, "En fazla 3 ürün ekleyebilirsiniz."),
});React Hook Form'da useFieldArray diziyi yönetir. fields satırları, append ve remove ekleme ve silmeyi verir:2
const { register, control } = useForm({
resolver: zodResolver(storeSchema),
defaultValues: { storeName: "", products: [{ name: "", price: "" }] },
});
const { fields, append, remove } = useFieldArray({ control, name: "products" });
return (
<fieldset>
<legend>Örnek ürünler</legend>
{fields.map((field, index) => (
<div key={field.id}>
<label htmlFor={`products-${index}-name`}>Ürün {index + 1} adı</label>
<input id={`products-${index}-name`} {...register(`products.${index}.name`)} />
<label htmlFor={`products-${index}-price`}>Ürün {index + 1} fiyatı (₺)</label>
<input
id={`products-${index}-price`}
inputMode="decimal"
{...register(`products.${index}.price`)}
/>
<button
type="button"
onClick={() => remove(index)}
disabled={fields.length === 1}
aria-label={`Ürün ${index + 1} kaldır`}
>
Kaldır
</button>
</div>
))}
<button
type="button"
onClick={() => append({ name: "", price: "" })}
disabled={fields.length >= 3}
>
Ürün ekle
</button>
</fieldset>
);Dokümanın kuralları:2
key={field.id},indexdeğil. RHF her satıra benzersiz biridverir.indexanahtar olursa ortadan bir satır silindiğinde React input'ları karıştırır. v8'de bu alanın adıfield.keyoluyor.appendtam nesne ister.append({ name: "" })gibi eksik veri vermeyin; satırın tüm alanlarını varsayılan değerleriyle verin.Aynı olayda işlem yığmayın. Bir tıklamada hem
appendhemremoveçağırmayın.Butonların
type="button"'ı unutulmasın. Formun içindeki her<button>varsayılan olarak gönderir.
append yeni satırın ilk alanına odaklanır. Klavye ve ekran okuyucu kullanıcısı için doğrudur; istemezseniz append(value, { shouldFocus: false }).2 Silme butonundaki aria-label her satırın butonunu ayırt eder; yalnızca "Kaldır" yazan beş buton ekran okuyucuda aynı duyulur.
Dizinin Kendi Hatası
min(1) ve max(3) tek bir satırın değil dizinin hatasıdır. Resolver bu hatayı satır hatalarıyla çakışmasın diye errors.products.root'a yazar. Dizi tamamen boşsa satır adı olmadığından hata doğrudan errors.products.message'a düşer. İkisini birlikte okuyun:23
const { errors } = useFormState({ control, name: "products" });
const listError = errors.products?.root?.message ?? errors.products?.message;
{listError && <p role="alert">{listError}</p>}Yukarıdaki form son satırın silinmesini ve dördüncü satırı engellediği için bu mesajlar normal kullanımda görünmez. Yine de şemadaki sınır sunucuyu korur.
Controller: Controlled Bileşenler
register DOM input'ları içindir. Kendi value ve onChange prop'larını isteyen bir bileşen (tarih seçici, yıldız puanı, bir UI kütüphanesinin select'i) için Controller kullanılır. render fonksiyonuna field (value, onChange, onBlur, name, ref) ve fieldState (error, invalid) gelir:4
<Controller
control={control}
name="rating"
render={({ field, fieldState }) => (
<StarRating
ref={field.ref}
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
aria-invalid={fieldState.invalid}
/>
)}
/>Üç ayrıntı:4
onBlur'u bağlayın; yoksaonTouchedveonBlurmodları bu alanda çalışmaz.ref'i bileşenin odaklanabilen öğesine verin; yoksa ilk hatalı alana odaklanma bu alanı atlar.Başlangıç değeri
undefinedolmasın.onChange(undefined)da çağırmayın; boş değer içinnullya da""kullanın.
Bir alanın değerini yalnızca dışarıdan güncellemek istiyorsanız Controller gerekmez; useForm'un values seçeneği yeterlidir.4
FormProvider ile Bölmek
Uzun bir formu component'lere bölünce register'ı ve control'ü prop olarak geçmek yorar. FormProvider form nesnesini Context'e koyar; alt component'ler useFormContext ile alır:5
// application-form.tsx
const form = useForm({ resolver: zodResolver(applicationSchema), defaultValues });
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} noValidate>
<StoreStep />
</form>
</FormProvider>
);// store-step.tsx
export function StoreStep() {
const { register, control } = useFormContext<ApplicationInput>();
const { fields, append, remove } = useFieldArray({ control, name: "products" });
// ...
}Context, form durumu değişince altındaki ağacı render eder. Hata göstermek gibi alana özel durumları useFormState({ name }) ile okuyun; yalnızca o alan değişince render olur. Hata gösterimi ve erişilebilirlik dersindeki Field component'i bu yüzden useFormState kullanır.56
Çok Adımlı Form
Mağaza, iletişim ve ödeme bilgileri üç adımda istensin. İki yaklaşım var:
| Tek form, adımlar | Adım başına form | |
|---|---|---|
| Yapı | Bir useForm, adım yalnızca neyin görüneceğini seçer | Her adım ayrı useForm, veri bir store'da toplanır |
| Geri dönme | Değerler formda durur | Store'dan geri yüklenir |
| Ne zaman | Adımlar aynı sayfada, veri birlikte gönderilecek | Adımlar ayrı URL'lerde, ara kayıt gerekiyor |
React Hook Form dokümanı ayrı sayfalı akışlar için ikinci yolu bir state kütüphanesiyle gösterir.7 Aynı sayfadaki bir sihirbaz için tek form daha basittir; bu derste o anlatılıyor.
Adımların alanları. Hangi alanın hangi adımda olduğunu şemanın yanında tutun:
export const steps = [
{ title: "Mağaza", fields: ["storeName", "taxNumber", "category", "products"] },
{ title: "İletişim", fields: ["contactName", "email", "phone"] },
{ title: "Ödeme", fields: ["iban", "acceptTerms"] },
] as const satisfies { title: string; fields: ApplicationField[] }[];İleri. trigger yalnızca verilen alanları doğrular ve sonucu döndürür. shouldFocus: true ilk hatalı alana odaklanır:8
const [step, setStep] = useState(0);
async function goNext() {
const valid = await form.trigger([...steps[step].fields], { shouldFocus: true });
if (valid) setStep((s) => s + 1);
}trigger("products") dizinin tüm satırlarını da doğrular.
Geri. Doğrulama gerekmez: setStep((s) => s - 1). Ekrandan kalkan adımın değerleri kaybolmaz; RHF varsayılan olarak unmount olan alanın değerini saklar (shouldUnregister: false).9
Gönder. Son adımda handleSubmit tüm şemayı doğrular. Sunucu önceki bir adımdaki alan için hata döndürürse o adıma geri gidin; nasıl yapılacağı Server Actions ile form dersinde.
Adım göstergesi. Mevcut adımı aria-current="step" ile işaretleyin ve her adımın başlığını bir <h2> yapın. Ekran okuyucu kullanıcısı nerede olduğunu duyar.
<ol aria-label="Adımlar">
{steps.map((s, index) => (
<li key={s.title} aria-current={index === step ? "step" : undefined}>
{index + 1}. {s.title}
</li>
))}
</ol>
<h2>{steps[step].title}</h2>Next.js'te Suspense Notu
Next.js 16'da cacheComponents açıkken sayfalar build sırasında önceden render edilir. useFieldArray satır id'lerini crypto.randomUUID() ile üretir. Next.js, rastgele değer üreten bir Client Component'i önceden render etmeyi reddeder ve build hatası verir: her ziyaretçiye aynı id gitmesin diye. Çözüm formu <Suspense> ile sarıp istek anında render etmektir:10
// app/page.tsx
export default function Page() {
return (
<main>
<h1>Satıcı başvurusu</h1>
<Suspense fallback={<p>Form yükleniyor...</p>}>
<ApplicationForm />
</Suspense>
</main>
);
}Kendin Dene
key={field.id}'yikey={index}yapın. Üç satıra farklı adlar yazıp ortadakini silin. Ne oluyor?append({ name: "" })yazın. TypeScript ne diyor?StoreStep'iFormProviderolmadan,registervecontrol'ü prop geçerek yazın.goNext'tekishouldFocus'u silip boş formla "İleri"ye basın. Odak nerede?İkinci adıma geçin, geri dönün. Ürün satırları ve değerleri duruyor mu?
useForm({ shouldUnregister: true })ile tekrar deneyin.
Detaylı Öğrenmek İçin
Resmi doküman: "useFieldArray"; tüm işlemler, kurallar ve örnekler.2
Resmi doküman: "Controller"; controlled bileşenleri bağlamak.4
Resmi doküman: "Advanced Usage"; sihirbaz formlar ve FormProvider performansı.7
Kaynaklar
Nesne dizisi, `min` ve `max`.
`fields`, `append`, `remove`, `field.id`, odak seçenekleri, kurallar ve `root` hatası.
Resolver hatalarının forma yerleştirilmesi.
`render`, `field`, `fieldState`, `onBlur`, `ref` ve `undefined` kuralı.
Form nesnesini Context'le paylaşmak.
Belirli bir alanın durumuna abone olmak.
Sihirbaz form ve FormProvider performansı.
Seçili alanları doğrulamak ve `shouldFocus`.
`shouldUnregister` ve unmount olan alanların değeri.
`crypto.randomUUID()` gibi rastgele değerlerin önceden render'da açıkça ele alınması ve Suspense ile istek anına bırakılması.
Son güncelleme tarihi: 22 Ekim 2026
