السلسلة: هذا الدرس جزء من سلسلة TanStack. اقرأ المقال الرئيسي.
كتابة نموذج React يكون type-safe، مُتحقَّقاً بنظافة، ومقروءاً، يبقى تمريناً مُحبِطاً في 2026. TanStack Form يقترح جواباً مختلفاً عن react-hook-form: يعتمد على معيار Standard Schema، يستخلص الأنواع من schema Zod (أو Valibot، أو ArkType) ويُقدّم API headless لا يهتمّ بالعرض ولا الـ style. تحافظ على تحكم كامل في JSX.
المتطلبات
- Node.js 22 LTS وnpm 10+
- مشروع React 19 مع TypeScript
strict: true - محرّر مع خادم TS نشط
- أساسيات Zod وتركيب schemas
الخطوة 1 — تثبيت TanStack Form وZod
npm install @tanstack/react-form zod
تأكّد @tanstack/react-form v1 مستقرّ، zod v4+. v4 يُحسّن الاستدلال ويُقدّم رسائل أخطاء i18n.
الخطوة 2 — تعريف schema Zod مصدر الحقيقة
// app/schemas/user.ts
import { z } from 'zod'
export const userSchema = z.object({
email: z
.string()
.min(1, "L'email est requis")
.email("Format d'email invalide"),
age: z
.number({ invalid_type_error: "L'age doit etre un nombre" })
.int()
.gte(13, 'Vous devez avoir au moins 13 ans'),
password: z.string().min(8, 'Au moins 8 caracteres'),
})
export type UserInput = z.infer<typeof userSchema>
النوع UserInput مُستدلّ مرة واحدة: يساوي { email: string; age: number; password: string }. تعريف واحد، استخدامان (عميل + خادم)، صفر تكرار.
الخطوة 3 — تهيئة hook useForm
// app/components/SignupForm.tsx
import { useForm } from '@tanstack/react-form'
import { userSchema } from '~/schemas/user'
export function SignupForm() {
const form = useForm({
defaultValues: { email: '', age: 0, password: '' },
validators: {
onChange: userSchema,
onSubmit: userSchema,
},
onSubmit: async ({ value }) => {
console.log('Soumission validee :', value)
},
})
return null
}
onChange: userSchema يُعيد التحقق عند كل ضربة مفتاح. onSubmit: userSchema يُعيد التحقق عند التقديم. handler onSubmit لا يُطلق إن أخفق التحقق.
الخطوة 4 — عرض الحقول مع form.Field
function SignupForm() {
return (
<form
onSubmit={(e) => {
e.preventDefault()
void form.handleSubmit()
}}
>
<form.Field name="email">
{(field) => (
<div>
<label htmlFor={field.name}>Email</label>
<input
id={field.name}
type="email"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{!field.state.meta.isValid && (
<em role="alert">{field.state.meta.errors.join(', ')}</em>
)}
</div>
)}
</form.Field>
<button type="submit">S'inscrire</button>
</form>
)
}
التنميط على اسم الحقل صارم: اكتب name="emial" وTypeScript يصرخ فوراً.
الخطوة 5 — إضافة حقل عددي وكلمة مرور
<form.Field name="age">
{(field) => (
<div>
<label htmlFor={field.name}>Age</label>
<input
id={field.name}
type="number"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{!field.state.meta.isValid && (
<em role="alert">{field.state.meta.errors.join(', ')}</em>
)}
</div>
)}
</form.Field>
<form.Field name="password">
{(field) => (
<div>
<label htmlFor={field.name}>Mot de passe</label>
<input
id={field.name}
type="password"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{!field.state.meta.isValid && (
<em role="alert">{field.state.meta.errors.join(', ')}</em>
)}
</div>
)}
</form.Field>
الحقل العددي يستخدم e.target.valueAsNumber الذي يُرجع رقماً مباشرة.
الخطوة 6 — تعطيل الزر بـ form.Subscribe
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit || isSubmitting}>
{isSubmitting ? 'Envoi...' : "S'inscrire"}
</button>
)}
/>
طالما النموذج يحوي خطأً، canSubmit false والزر يبقى رمادياً. خلال التقديم، isSubmitting ينقلب true.
الخطوة 7 — التقديم نحو server function
// app/server/auth.ts
import { createServerFn } from '@tanstack/react-start'
import { userSchema } from '~/schemas/user'
export const signupServerFn = createServerFn({ method: 'POST' })
.inputValidator(userSchema)
.handler(async ({ data }) => {
return { ok: true, userId: 'usr_' + Math.random().toString(36).slice(2) }
})
// المكوّن
import { signupServerFn } from '~/server/auth'
const form = useForm({
defaultValues: { email: '', age: 0, password: '' },
validators: { onChange: userSchema, onSubmit: userSchema },
onSubmit: async ({ value }) => {
const result = await signupServerFn({ data: value })
console.log('Inscrit :', result.userId)
},
})
نفس userSchema يخدم التحقق جانب العميل (UX) وجانب الخادم (أمان). إن استدعى شخص ما server function بـ payload معدَّل، Zod يرفض الطلب قبل بلوغ قاعدة البيانات.
الخطوة 8 — عرض خطأ خادم عام
import { useState } from 'react'
function SignupForm() {
const [serverError, setServerError] = useState<string | null>(null)
const form = useForm({
defaultValues: { email: '', age: 0, password: '' },
validators: { onChange: userSchema, onSubmit: userSchema },
onSubmit: async ({ value }) => {
setServerError(null)
try {
await signupServerFn({ data: value })
} catch (e) {
setServerError(e instanceof Error ? e.message : 'Erreur inconnue')
}
},
})
return (
<form onSubmit={(e) => { e.preventDefault(); void form.handleSubmit() }}>
{serverError && <div role="alert">{serverError}</div>}
</form>
)
}
لحقن الخطأ في حقل محدّد، انظر field.setMeta({ errors: [...] }).
مقارنة TanStack Form مقابل react-hook-form
| المعيار | TanStack Form | react-hook-form |
|---|---|---|
| Type-safety | أصلي، بلا any |
قوي مع useForm<Type>() |
| Validation Zod | Standard Schema، بلا adapter | عبر @hookform/resolvers/zod |
| API | Render-prop form.Field |
Hooks register وController |
| الأداء | Selectors دقيقة (Subscribe) |
إعادة عرض معزولة لكل حقل |
| Async validation | validators async أصلية | عبر resolver |
| النظام البيئي | ناشئ في 2026 | واسع جداً |
| الحزمة | ~10 KB gzippé | ~9 KB gzippé |
أخطاء شائعة
| الخطأ | السبب | الحل |
|---|---|---|
field.state.value undefined |
لا قيمة افتراضية | أضف الحقل في defaultValues |
| Type mismatch على name | خطأ كتابة في اسم الحقل | اعتمد على autocomplete TS |
| حقل عددي يستلم سلسلة | e.target.value بدل valueAsNumber |
استخدم e.target.valueAsNumber |
| الزر دائماً قابل للنقر | لا form.Subscribe على canSubmit |
وصّل الاشتراك الدقيق |
| Validation لا تُطلق | validators غير مُمرَّرة | عرّف validators.onChange أو onSubmit |
| خطأ خادم مفقود | لا try/catch في onSubmit |
خزّن الخطأ في useState محلي |
أسئلة شائعة
هل يمكن TanStack Form بلا Zod؟ نعم. validators تقبل أيضاً دوال كلاسيكية تُرجع undefined أو رسالة خطأ. لكن الفائدة الرئيسية الاستدلال من schema، فـ Zod أو Valibot يبقى التركيب الاصطلاحي.
كيف نُدير حقل مصفوفة (مثل قائمة tags)؟ form.Field يقبل مسارات متداخلة (tags.0، tags.1)، أو استخدم form.useField({ name: 'tags' }) لمعالجة المصفوفة كاملة بـ pushValue وremoveValue.
كيف نُعيد تعيين نموذج بعد التقديم؟ form.reset() يُعيد القيم الافتراضية. استدعه بعد await server function في onSubmit.
هل يمكن ربط النموذج بحالة عالمية (Zustand، Redux)؟ نعم لكن غالباً غير مفيد. TanStack Form يدير حالته الخاصة؛ الحالة العالمية تستلم البيانات فقط عند التقديم.
هل validation async تشتغل؟ نعم، عبر validators: { onChangeAsync: async ({ value }) => { ... return errorString } }. فكّر بـ debounce على جانب المكوّن لتجنّب ضرب API عند كل ضربة مفتاح.