البرمجة مبتدئ 11 دقيقة

كيفية بناء نموذج تفاعلي مع التحقق في React

النماذج المتحكم بها (Controlled Forms) — حيث يمتلك React قيمة كل حقل مدخل — هي النمط القياسي في React. المقايضة هي كود أكثر مقارنةً بالمدخلات غير المتحكم بها، لكن تحصل على ردود فعل تحقق فورية، ومنطق سهل بين الحقول، وسلوك submit متوقع. يبني هذا الدليل نموذجاً كاملاً مع أخطاء على مستوى الحقل، وزر submit معطّل أثناء الإرسال، وإعادة ضبط نظيفة بعد النجاح. في النهاية، ستعرف بالضبط متى يكون من المنطقي استخدام react-hook-form بدلاً من ذلك.

الخطوات

  1. 1

    اختر: كائن state واحد أم useState منفصلة

    للنماذج التي تحتوي أكثر من 2–3 حقول، خزّن قيم جميع الحقول في كائن state واحد. يجعل إعادة الضبط أمراً تافهاً (استدعاء setState واحد)، ومعالج onChange قابل للإعادة الاستخدام عبر جميع الحقول. استدعاءات useState المنفصلة لكل حقل تصبح معقدة بسرعة وتشتت منطق إعادة الضبط.

    jsx
    const 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. 2

    ربط المدخلات المتحكم بها

    المدخل المتحكم به يحتوي على value (من الـ state) وonChange (يحدّث الـ state). بدون value، المدخل غير متحكم به ولا يستطيع React تتبع محتواه. بدون onChange، المدخل للقراءة فقط ولا يستطيع المستخدم الكتابة فيه.

    خاصية name يجب أن تطابق المفتاح في formData — هكذا يعرف المعالج الواحد أي حقل يحدّث.

    jsx
    return (
      <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. 3

    كتابة دالة التحقق

    احتفظ بمنطق التحقق في دالة نقية تأخذ قيم النموذج وتُعيد كائن أخطاء. يجعل هذا الاختبار بشكل مستقل عن المكوّن سهلاً. أعد كائناً فارغاً عندما يكون النموذج صالحاً.

    javascript
    function 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. 4

    التحقق عند مغادرة الحقل، عرض الأخطاء مباشرةً

    تحقق عند blur (عندما يفقد الحقل التركيز)، وليس عند كل ضغطة مفتاح. إظهار الأخطاء بينما المستخدم يكتب مزعج. عند الإرسال، تحقق من جميع الحقول دفعة واحدة بغض النظر عن حالة اللمس.

    خزّن الأخطاء في state واعرضها فوراً أسفل كل حقل. اربطها بالمدخل عبر aria-describedby لإمكانية الوصول.

    jsx
    const [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. 5

    معالجة الإرسال مع التحقق الكامل

    عند الإرسال، تحقق من كل حقل (بتجاهل حالة اللمس — المستخدم ضغط إرسال، أظهر جميع الأخطاء). إذا كانت هناك أي أخطاء، ركّز على الحقل الأول غير الصالح واخرج. وإلا، تابع مع استدعاء API.

    jsx
    const [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. 6

    تعطيل زر الإرسال أثناء الإرسال

    امنع الإرسال المزدوج بتعطيل الزر بينما الطلب في الهواء. غيّر النص لإعطاء المستخدم ملاحظة بأن شيئاً يحدث. أعد التفعيل عند النجاح أو الخطأ.

    jsx
    <button
      type="submit"
      disabled={submitting}
      aria-busy={submitting}
    >
      {submitting ? 'جاري إنشاء الحساب...' : 'إنشاء حساب'}
    </button>
    
    {/* اعرض أخطاء مستوى الخادم فوق الزر */}
    {errors.form && (
      <p role="alert" style={{ color: 'red' }}>{errors.form}</p>
    )}
  7. 7

    اجمع كل شيء معاً

    هذا هو المكوّن الكامل. لاحظ كيف يخدم handleChange الواحد جميع المدخلات، ودالة التحقق منفصلة وقابلة للاختبار، ومعالج الإرسال يغطي المسار السعيد الكامل ومسار الخطأ.

    jsx
    import { 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. 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>&lt;form&gt;</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 مع تلك الحالات بكفاءة دون عبء المدخلات المتحكم بها.

#React #Forms #Validation
العودة إلى جميع الأدلة

هل تحتاج مساعدة في مشروعك؟

احجز استشارة مجانية لمدة 30 دقيقة لمناقشة تحدياتك التقنية واستكشاف الحلول معًا.