الخطوات
-
1
اختر: كائن state واحد أم useState منفصلة
للنماذج التي تحتوي أكثر من 2–3 حقول، خزّن قيم جميع الحقول في كائن state واحد. يجعل إعادة الضبط أمراً تافهاً (استدعاء
setStateواحد)، ومعالجonChangeقابل للإعادة الاستخدام عبر جميع الحقول. استدعاءاتuseStateالمنفصلة لكل حقل تصبح معقدة بسرعة وتشتت منطق إعادة الضبط.jsxconst initialValues = { name: '', email: '', password: '', role: 'user', }; function SignupForm() { const [formData, setFormData] = useState(initialValues); function handleChange(e) { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); } // جميع المدخلات تشترك في هذا المعالج الواحد عبر خاصية `name` } -
2
ربط المدخلات المتحكم بها
المدخل المتحكم به يحتوي على
value(من الـ state) وonChange(يحدّث الـ state). بدونvalue، المدخل غير متحكم به ولا يستطيع React تتبع محتواه. بدونonChange، المدخل للقراءة فقط ولا يستطيع المستخدم الكتابة فيه.خاصية
nameيجب أن تطابق المفتاح فيformData— هكذا يعرف المعالج الواحد أي حقل يحدّث.jsxreturn ( <form onSubmit={handleSubmit}> <div> <label htmlFor="name">الاسم</label> <input id="name" name="name" type="text" value={formData.name} onChange={handleChange} /> </div> <div> <label htmlFor="email">البريد الإلكتروني</label> <input id="email" name="email" type="email" value={formData.email} onChange={handleChange} /> </div> <div> <label htmlFor="password">كلمة المرور</label> <input id="password" name="password" type="password" value={formData.password} onChange={handleChange} /> </div> <button type="submit">إنشاء حساب</button> </form> ); -
3
كتابة دالة التحقق
احتفظ بمنطق التحقق في دالة نقية تأخذ قيم النموذج وتُعيد كائن أخطاء. يجعل هذا الاختبار بشكل مستقل عن المكوّن سهلاً. أعد كائناً فارغاً عندما يكون النموذج صالحاً.
javascriptfunction validate(values) { const errors = {}; if (!values.name.trim()) { errors.name = 'الاسم مطلوب.'; } if (!values.email.trim()) { errors.email = 'البريد الإلكتروني مطلوب.'; } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) { errors.email = 'أدخل بريداً إلكترونياً صالحاً.'; } if (!values.password) { errors.password = 'كلمة المرور مطلوبة.'; } else if (values.password.length < 8) { errors.password = 'كلمة المرور يجب أن تكون 8 أحرف على الأقل.'; } return errors; // كائن فارغ = صالح } -
4
التحقق عند مغادرة الحقل، عرض الأخطاء مباشرةً
تحقق عند blur (عندما يفقد الحقل التركيز)، وليس عند كل ضغطة مفتاح. إظهار الأخطاء بينما المستخدم يكتب مزعج. عند الإرسال، تحقق من جميع الحقول دفعة واحدة بغض النظر عن حالة اللمس.
خزّن الأخطاء في state واعرضها فوراً أسفل كل حقل. اربطها بالمدخل عبر
aria-describedbyلإمكانية الوصول.jsxconst [errors, setErrors] = useState({}); const [touched, setTouched] = useState({}); function handleBlur(e) { const { name } = e.target; setTouched(prev => ({ ...prev, [name]: true })); // تحقق من الحقل الذي تم لمسه فقط const fieldErrors = validate(formData); setErrors(prev => ({ ...prev, [name]: fieldErrors[name] })); } // في JSX، أسفل كل مدخل: // {touched.email && errors.email && ( // <span id="email-error" role="alert">{errors.email}</span> // )} // // على المدخل نفسه: // aria-describedby={errors.email ? 'email-error' : undefined} // aria-invalid={!!errors.email} -
5
معالجة الإرسال مع التحقق الكامل
عند الإرسال، تحقق من كل حقل (بتجاهل حالة اللمس — المستخدم ضغط إرسال، أظهر جميع الأخطاء). إذا كانت هناك أي أخطاء، ركّز على الحقل الأول غير الصالح واخرج. وإلا، تابع مع استدعاء API.
jsxconst [submitting, setSubmitting] = useState(false); async function handleSubmit(e) { e.preventDefault(); const fieldErrors = validate(formData); if (Object.keys(fieldErrors).length > 0) { setErrors(fieldErrors); // اجعل جميع الحقول مُلمَسة حتى تظهر الأخطاء setTouched({ name: true, email: true, password: true, role: true }); return; } setSubmitting(true); try { await api.signup(formData); setFormData(initialValues); // امسح النموذج عند النجاح setErrors({}); setTouched({}); // انتقل أو اعرض رسالة نجاح } catch (err) { setErrors({ form: err.message }); // خطأ على مستوى الخادم } finally { setSubmitting(false); } } -
6
تعطيل زر الإرسال أثناء الإرسال
امنع الإرسال المزدوج بتعطيل الزر بينما الطلب في الهواء. غيّر النص لإعطاء المستخدم ملاحظة بأن شيئاً يحدث. أعد التفعيل عند النجاح أو الخطأ.
jsx<button type="submit" disabled={submitting} aria-busy={submitting} > {submitting ? 'جاري إنشاء الحساب...' : 'إنشاء حساب'} </button> {/* اعرض أخطاء مستوى الخادم فوق الزر */} {errors.form && ( <p role="alert" style={{ color: 'red' }}>{errors.form}</p> )} -
7
اجمع كل شيء معاً
هذا هو المكوّن الكامل. لاحظ كيف يخدم
handleChangeالواحد جميع المدخلات، ودالة التحقق منفصلة وقابلة للاختبار، ومعالج الإرسال يغطي المسار السعيد الكامل ومسار الخطأ.jsximport { useState } from 'react'; const initialValues = { name: '', email: '', password: '' }; function validate(v) { const e = {}; if (!v.name.trim()) e.name = 'الاسم مطلوب.'; if (!v.email.trim()) e.email = 'البريد الإلكتروني مطلوب.'; else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.email)) e.email = 'أدخل بريداً صالحاً.'; if (!v.password) e.password = 'كلمة المرور مطلوبة.'; else if (v.password.length < 8) e.password = '8 أحرف على الأقل.'; return e; } export function SignupForm() { const [formData, setFormData] = useState(initialValues); const [errors, setErrors] = useState({}); const [touched, setTouched] = useState({}); const [submitting, setSubmitting] = useState(false); const [success, setSuccess] = useState(false); const handleChange = e => { const { name, value } = e.target; setFormData(p => ({ ...p, [name]: value })); }; const handleBlur = e => { const { name } = e.target; setTouched(p => ({ ...p, [name]: true })); setErrors(p => ({ ...p, [name]: validate(formData)[name] })); }; const handleSubmit = async e => { e.preventDefault(); const errs = validate(formData); if (Object.keys(errs).length) { setErrors(errs); setTouched({ name: true, email: true, password: true }); return; } setSubmitting(true); try { await fakeApi(formData); setFormData(initialValues); setErrors({}); setTouched({}); setSuccess(true); } catch (err) { setErrors({ form: err.message }); } finally { setSubmitting(false); } }; if (success) return <p>تم إنشاء الحساب بنجاح!</p>; return ( <form onSubmit={handleSubmit} noValidate> {['name', 'email', 'password'].map(field => ( <div key={field}> <label htmlFor={field}>{field}</label> <input id={field} name={field} type={field === 'password' ? 'password' : field === 'email' ? 'email' : 'text'} value={formData[field]} onChange={handleChange} onBlur={handleBlur} aria-invalid={!!(touched[field] && errors[field])} aria-describedby={errors[field] ? `${field}-err` : undefined} /> {touched[field] && errors[field] && ( <span id={`${field}-err`} role="alert">{errors[field]}</span> )} </div> ))} {errors.form && <p role="alert">{errors.form}</p>} <button type="submit" disabled={submitting}> {submitting ? 'جاري الحفظ...' : 'إنشاء حساب'} </button> </form> ); } -
8
انتقل إلى react-hook-form عند الحاجة
النمط المتحكم به أعلاه كافٍ تماماً للنماذج التي تحتوي حتى ~8 حقول وقواعد تحقق بسيطة. عندما تحتاج المضي أبعد — مصفوفات حقول متعشقة بعمق، تحقق معقد بين الحقول، تحميل ملفات، نماذج متعددة الخطوات، أو نماذج حساسة للأداء بحقول كثيرة —
react-hook-formهو الخيار القياسي. يستخدم مدخلات غير متحكم بها داخلياً (لا إعادة رسم عند كل ضغطة مفتاح)، ويتكامل مع مكتبات الـ schema مثل Zod أو Yup، وله سطح API أصغر بكثير من البدائل مثل Formik.jsx// نسخة react-hook-form لنفس النموذج import { useForm } from 'react-hook-form'; function SignupForm() { const { register, handleSubmit, formState: { errors, isSubmitting }, reset, } = useForm(); const onSubmit = async (data) => { await fakeApi(data); reset(); }; return ( <form onSubmit={handleSubmit(onSubmit)} noValidate> <input type="text" {...register('name', { required: 'الاسم مطلوب.' })} aria-invalid={!!errors.name} /> {errors.name && <span role="alert">{errors.name.message}</span>} <input type="email" {...register('email', { required: 'البريد الإلكتروني مطلوب.', pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: 'بريد غير صالح.' }, })} aria-invalid={!!errors.email} /> {errors.email && <span role="alert">{errors.email.message}</span>} <button type="submit" disabled={isSubmitting}> {isSubmitting ? 'جاري الحفظ...' : 'إنشاء حساب'} </button> </form> ); }
نصائح ومحاذير
- أضف دائماً <code>noValidate</code> لعنصر <code><form></code> عند التحقق المخصص. وإلا ستتعارض واجهة التحقق المدمجة في المتصفح مع رسائل الخطأ الخاصة بك.
- خاصية <code>name</code> على كل مدخل ليست اختيارية — هي المفتاح الذي يربط <code>handleChange</code> العام بالحقل الصحيح في <code>formData</code>.
- للـ checkboxes، استخدم <code>e.target.checked</code> بدلاً من <code>e.target.value</code>. للـ multi-select، استخدم <code>Array.from(e.target.selectedOptions, o => o.value)</code>.
- تحقق أيضاً على الخادم. التحقق من جانب العميل هو تحسين لتجربة المستخدم وليس إجراء أمنياً — يمكن لأي شخص تجاوزه.
- عند استخدام <code>react-hook-form</code> مع مكتبة UI (MUI، Chakra)، استخدم wrapper الـ <code>Controller</code> للمدخلات التي لا تكشف عن <code>ref</code> أصلي.
خاتمة
نموذج React المتحكم به هو عدة قطع متحركة — كائن state واحد للقيم، وواحد للأخطاء، وواحد للحقول المُلمَسة، وواحد لحالة الإرسال — لكن لكل قطعة وظيفة واحدة واضحة. أتقن هذا النمط وستتعامل مع 90% من النماذج في التطبيقات الحقيقية. عندما تنمو النماذج أبعد من ذلك — مصفوفات الحقول، تدفقات المعالج، التبعيات المعقدة — يتعامل react-hook-form مع تلك الحالات بكفاءة دون عبء المدخلات المتحكم بها.