در این راهنما ساخت فرم با TanStack Form را بررسی میکنیم؛ از ساخت فرم با کامپوننت <Field /> گرفته تا اعتبارسنجی Schema با Zod، مدیریت خطا و بهبود دسترسپذیری.
ابتدا فرم زیر را میسازیم. این فرم شامل یک ورودی متنی و یک Textarea است. هنگام ارسال فرم، دادهها اعتبارسنجی میشوند و خطاهای احتمالی نمایش داده خواهند شد.
نکته: برای این Demo، اعتبارسنجی پیشفرض مرورگر را عمداً غیرفعال کردهایم تا نحوهٔ کار Schema Validation و مدیریت خطاها در TanStack Form مشخص باشد. در پروژهٔ واقعی بهتر است اعتبارسنجی پایهٔ مرورگر را نیز فعال نگه دارید.
این فرم از TanStack Form برای مدیریت قدرتمند و Headless فرم استفاده میکند. فرم را با کامپوننت <Field /> میسازیم تا کنترل کاملی روی Markup و Styling داشته باشیم.
useForm در TanStack Form برای مدیریت وضعیت فرم.form.Field با الگوی Render Prop برای ورودیهای Controlled.<Field /> برای ساخت فرمهای دسترسپذیر.در مثال پایه، از TanStack Form و کامپوننت <Field /> برای ساخت فرم استفاده میکنیم.
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<FieldGroup>
<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}>عنوان باگ</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="دکمهٔ ورود در موبایل کار نمیکند"
autoComplete="off"
/>
<FieldDescription>
عنوانی کوتاه و واضح برای باگ وارد کنید.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>
</FieldGroup>
<Button type="submit">ارسال</Button>
</form>ابتدا ساختار فرم را با یک Schema از Zod تعریف میکنیم.
نکته: این مثال از zod v3 برای اعتبارسنجی Schema استفاده میکند.
TanStack Form از Zod و سایر کتابخانههای اعتبارسنجی مبتنی بر Standard Schema از
طریق API مربوط به Validatorها پشتیبانی میکند.
import * as z from "zod"
const formSchema = z.object({
title: z
.string()
.min(5, "عنوان باگ باید حداقل ۵ کاراکتر باشد.")
.max(32, "عنوان باگ باید حداکثر ۳۲ کاراکتر باشد."),
description: z
.string()
.min(20, "توضیحات باید حداقل ۲۰ کاراکتر باشد.")
.max(100, "توضیحات باید حداکثر ۱۰۰ کاراکتر باشد."),
})برای ساخت نمونهٔ فرم همراه با اعتبارسنجی Zod از Hook useForm در TanStack Form استفاده کنید.
import { useForm } from "@tanstack/react-form"
import { toast } from "sonner"
import * as z from "zod"
const formSchema = z.object({
// ...
})
export function BugReportForm() {
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
toast.success("فرم با موفقیت ارسال شد")
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
{/* ... */}
</form>
)
}در اینجا از onSubmit برای اعتبارسنجی دادههای فرم استفاده میکنیم. TanStack Form از حالتهای اعتبارسنجی دیگری نیز پشتیبانی میکند که میتوانید آنها را در مستندات مطالعه کنید.
حالا میتوانیم فرم را با استفاده از کامپوننت form.Field در TanStack Form و کامپوننت <Field /> بسازیم.
تمام شد. حالا یک فرم کاملاً دسترسپذیر با اعتبارسنجی سمت Client دارید.
هنگام ارسال فرم، تابع onSubmit با دادههای اعتبارسنجیشدهٔ فرم اجرا میشود. اگر دادهها معتبر نباشند، TanStack Form خطاها را در کنار فیلد مربوطه نمایش میدهد.
TanStack Form با استفاده از Schema تعریفشده در Zod، دادههای فرم را اعتبارسنجی میکند. اعتبارسنجی میتواند همزمان با وارد کردن اطلاعات توسط کاربر انجام شود.
import { useForm } from "@tanstack/react-form"
const formSchema = z.object({
// ...
})
export function BugReportForm() {
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
console.log(value)
},
})
return <form onSubmit={/* ... */}>{/* ... */}</form>
}TanStack Form از طریق گزینهٔ validators از استراتژیهای مختلف اعتبارسنجی پشتیبانی میکند:
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
onChange: formSchema,
onBlur: formSchema,
},
})برای نمایش خطا در کنار فیلد از <FieldError /> استفاده کنید. برای Styling و دسترسپذیری:
data-invalid را به کامپوننت <Field /> اضافه کنید.aria-invalid را به کنترل فرم، مانند <Input />، <SelectTrigger />، <Checkbox /> و موارد مشابه اضافه کنید.<form.Field
name="email"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>ایمیل</FieldLabel>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
type="email"
aria-invalid={isInvalid}
/>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>field.state.value و field.handleChange در کامپوننت <Input /> استفاده کنید.aria-invalid را به <Input /> و Prop data-invalid را به <Field /> اضافه کنید.<form.Field
name="username"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor="form-tanstack-input-username">
نامکاربری
</FieldLabel>
<Input
id="form-tanstack-input-username"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="نامکاربری"
autoComplete="username"
/>
<FieldDescription>
این نام بهصورت عمومی در پروفایل شما نمایش داده میشود و باید بین ۳ تا
۱۰ کاراکتر باشد و فقط شامل حروف، اعداد و Underscore باشد.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>field.state.value و field.handleChange در کامپوننت <Textarea /> استفاده کنید.aria-invalid را به <Textarea /> و Prop data-invalid را به <Field /> اضافه کنید.<form.Field
name="about"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor="form-tanstack-textarea-about">
بیشتر دربارهٔ شما
</FieldLabel>
<Textarea
id="form-tanstack-textarea-about"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="من مهندس نرمافزار هستم..."
className="min-h-16"
/>
<FieldDescription>
اطلاعات بیشتری دربارهٔ خودتان بنویسید. از این اطلاعات برای شخصیسازی
تجربهٔ شما استفاده خواهد شد.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>field.state.value و field.handleChange در کامپوننت <Select /> استفاده کنید.aria-invalid را به <SelectTrigger /> و Prop data-invalid را به <Field /> اضافه کنید.<form.Field
name="language"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="responsive" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-select-language">
زبان گفتاری
</FieldLabel>
<FieldDescription>
برای دریافت نتیجهٔ بهتر، زبانی را انتخاب کنید که به آن صحبت میکنید.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldContent>
<Select
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
>
<SelectTrigger
id="form-tanstack-select-language"
aria-invalid={isInvalid}
className="min-w-[120px]"
dir="rtl"
>
<SelectValue placeholder="انتخاب کنید" />
</SelectTrigger>
<SelectContent position="item-aligned" dir="rtl">
<SelectItem value="auto">خودکار</SelectItem>
<SelectItem value="en">انگلیسی</SelectItem>
</SelectContent>
</Select>
</Field>
)
}}
/>field.state.value و field.handleChange در کامپوننت <Checkbox /> استفاده کنید.aria-invalid را به <Checkbox /> و Prop data-invalid را به <Field /> اضافه کنید.mode="array" در کامپوننت <form.Field /> و Helperهای آرایهای TanStack Form استفاده کنید.data-slot="checkbox-group" را به کامپوننت <FieldGroup /> اضافه کنید.<form.Field
name="tasks"
mode="array"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<FieldSet>
<FieldLegend variant="label">وظایف</FieldLegend>
<FieldDescription>
برای بهروزرسانی وظایفی که ایجاد کردهاید، اعلان دریافت کنید.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{tasks.map((task) => (
<Field
key={task.id}
orientation="horizontal"
data-invalid={isInvalid}
>
<Checkbox
id={`form-tanstack-checkbox-${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)
}
}
}}
/>
<FieldLabel
htmlFor={`form-tanstack-checkbox-${task.id}`}
className="font-normal"
>
{task.label}
</FieldLabel>
</Field>
))}
</FieldGroup>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldSet>
)
}}
/>field.state.value و field.handleChange در کامپوننت <RadioGroup /> استفاده کنید.aria-invalid را به <RadioGroupItem /> و Prop data-invalid را به <Field /> اضافه کنید.<form.Field
name="plan"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<FieldSet>
<FieldLegend>پلن</FieldLegend>
<FieldDescription>
هر زمان بخواهید میتوانید پلن خود را ارتقا یا کاهش دهید.
</FieldDescription>
<RadioGroup
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
dir="rtl"
>
{plans.map((plan) => (
<FieldLabel
key={plan.id}
htmlFor={`form-tanstack-radiogroup-${plan.id}`}
>
<Field orientation="horizontal" data-invalid={isInvalid}>
<FieldContent>
<FieldTitle>{plan.title}</FieldTitle>
<FieldDescription>{plan.description}</FieldDescription>
</FieldContent>
<RadioGroupItem
value={plan.id}
id={`form-tanstack-radiogroup-${plan.id}`}
aria-invalid={isInvalid}
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldSet>
)
}}
/>field.state.value و field.handleChange در کامپوننت <Switch /> استفاده کنید.aria-invalid را به <Switch /> و Prop data-invalid را به <Field /> اضافه کنید.<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="form-tanstack-switch-twoFactor">
احراز هویت چندمرحلهای
</FieldLabel>
<FieldDescription>
احراز هویت چندمرحلهای را برای افزایش امنیت حساب فعال کنید.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldContent>
<Switch
id="form-tanstack-switch-twoFactor"
name={field.name}
checked={field.state.value}
onCheckedChange={field.handleChange}
aria-invalid={isInvalid}
/>
</Field>
)
}}
/>در این مثال یک فرم پیچیدهتر با چندین فیلد و اعتبارسنجی میسازیم.
برای بازنشانی فرم به مقادیر پیشفرض، از form.reset() استفاده کنید.
<Button type="button" variant="outline" onClick={() => form.reset()}>
بازنشانی
</Button>TanStack Form با استفاده از mode="array" مدیریت قدرتمندی برای فیلدهای آرایهای ارائه میدهد. با این قابلیت میتوانید آیتمهای آرایه را بهصورت پویا اضافه، حذف و ویرایش کنید و همزمان از اعتبارسنجی کامل آنها بهره ببرید.
در این مثال نحوهٔ مدیریت چند آدرس ایمیل با استفاده از فیلدهای آرایهای را میبینید. کاربر میتواند حداکثر ۵ آدرس ایمیل اضافه کند، هر آدرس را بهصورت جداگانه حذف کند و هر آدرس نیز بهصورت مستقل اعتبارسنجی میشود.
برای فعال کردن مدیریت فیلدهای آرایهای، از mode="array" در فیلد والد استفاده کنید.
<form.Field
name="emails"
mode="array"
children={(field) => {
return (
<FieldSet>
<FieldLegend variant="label">آدرسهای ایمیل</FieldLegend>
<FieldDescription>
حداکثر ۵ آدرس ایمیل اضافه کنید تا بتوانیم با شما در تماس باشیم.
</FieldDescription>
<FieldGroup>
{field.state.value.map((_, index) => (
// فیلد تو در تو برای هر آیتم آرایه
))}
</FieldGroup>
</FieldSet>
)
}}
/>برای دسترسی به آیتمهای آرایه از Notation براکت استفاده کنید: fieldName[index].propertyName.
در این مثال از InputGroup برای نمایش دکمهٔ حذف در کنار ورودی استفاده شده است.
<form.Field
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={`form-tanstack-array-email-${index}`}
name={subField.name}
value={subField.state.value}
onBlur={subField.handleBlur}
onChange={(e) => subField.handleChange(e.target.value)}
aria-invalid={isSubFieldInvalid}
placeholder="[email protected]"
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={`حذف ایمیل ${index + 1}`}
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
)}
</InputGroup>
{isSubFieldInvalid && (
<FieldError errors={subField.state.meta.errors} />
)}
</FieldContent>
</Field>
)
}}
/>برای اضافه کردن آیتم به فیلد آرایهای از field.pushValue(item) استفاده کنید. زمانی که تعداد آیتمها به حداکثر مجاز برسد، میتوانید دکمه را غیرفعال کنید.
<Button
type="button"
variant="outline"
size="sm"
onClick={() => field.pushValue({ address: "" })}
disabled={field.state.value.length >= 5}
>
افزودن آدرس ایمیل
</Button>برای حذف یک آیتم از فیلد آرایهای از field.removeValue(index) استفاده کنید. میتوانید دکمهٔ حذف را فقط زمانی نمایش دهید که بیش از یک آیتم وجود داشته باشد.
{
field.state.value.length > 1 && (
<InputGroupButton
onClick={() => field.removeValue(index)}
aria-label={`حذف ایمیل ${index + 1}`}
>
<XIcon />
</InputGroupButton>
)
}برای اعتبارسنجی فیلدهای آرایهای از متدهای آرایه در Zod استفاده کنید.
const formSchema = z.object({
emails: z
.array(
z.object({
address: z.string().email("یک آدرس ایمیل معتبر وارد کنید."),
})
)
.min(1, "حداقل یک آدرس ایمیل اضافه کنید.")
.max(5, "حداکثر میتوانید ۵ آدرس ایمیل اضافه کنید."),
})