EduFlow
أساسيات React
⁦0%⁩

الوحدة 3 · الـ State والأحداث

النماذج والـ controlled inputs

24 دقراءة 4 دبيئة تجريب الكود

الدرس في لمحة

ابنِ نموذج إضافة الأفلام في Watchlist باستخدام الـ controlled inputs، وتعامل مع الإرسال والتحقّق من المدخلات، واعرف متى تكون الـ form actions وuseActionState في React 19 الخيار الأنسب.

  1. 1الـ controlled input
  2. 2الإرسال
  3. 3أنواع أخرى من الحقول
  4. 4React 19: الـ form actions

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

كل تطبيق مفيد يستقبل مدخلات. في Watchlist يعني ذلك نموذجًا: اكتب عنوانًا، وسنة إن أردت، واضغط Enter أو انقر Add، فيظهر الفيلم في القائمة. النماذج هي المكان الذي يلتقي فيه الـ state بالأحداث، وفيها يعيش معظم أخطاء المبتدئين: حقول ترفض الكتابة، وصفحات يُعاد تحميلها عند الإرسال، وأرقام تُلصق ببعضها كالنصوص. ولكل واحد منها سبب واضح.

الـ controlled input

في HTML العادي يملك الحقل قيمته، وأنت تقرؤها حين تحتاجها. أمّا في React فالأسلوب المعتاد يقلب ذلك: الـ state يملك القيمة، والحقل يعرضها.

src/components/AddMovieForm.jsx
import { useState } from 'react';

export default function AddMovieForm() {
  const [title, setTitle] = useState('');

  return (
    <form>
      <label htmlFor="title">Title</label>
      <input
        id="title"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
      />
      <p>{title.length}/80 characters</p>
    </form>
  );
}

هذا هو الـ controlled input (الحقل المتحكَّم به). كل ضغطة مفتاح تُطلق onChange، فيخزّن المعالج النص الجديد في الـ state، وتعيد React الـ render، ويعرض الحقل الـ state. يبدو هذا التفافًا، لكن النص الحالي صار الآن متغيّرًا عاديًا. يمكنك عرض عدد الحروف، والتحقّق أثناء الكتابة، وتعطيل زر، أو مسح الحقل بضبط الـ state على ''.

لاحظ أن onChange في React يعمل مع كل ضغطة مفتاح، مثل حدث input في الـ DOM، لا فقط حين يفقد الحقل التركيز.

الإرسال

تعامل مع الإرسال على النموذج، لا على نقرة الزر:

src/components/AddMovieForm.jsx
export default function AddMovieForm({ onAdd }) {
  const [title, setTitle] = useState('');
  const trimmed = title.trim();

  function handleSubmit(e) {
    e.preventDefault();
    if (!trimmed) return;
    onAdd(trimmed);
    setTitle('');
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="title">Title</label>
      <input id="title" value={title} onChange={(e) => setTitle(e.target.value)} />
      <button type="submit" disabled={!trimmed}>Add</button>
    </form>
  );
}

وجود onSubmit على النموذج يعني أن الضغط على Enter داخل الحقل يعمل، والزر يعمل، والتقنيات المساعدة تفهم النموذج، كل ذلك من دون كود إضافي. وتوقف e.preventDefault() السلوك الافتراضي للمتصفّح، وهو إرسال النموذج إلى رابط وإعادة تحميل الصفحة. ويبلّغ الـ component عن العنوان الجديد عبر onAdd، ويمسح الحقل بإعادة ضبط الـ state.

والتحقّق يأتي من الـ state أيضًا. تُشتقّ trimmed في كل render، فيكون الزر معطّلًا بالضبط ما دام العنوان فارغًا، بما في ذلك العنوان المكوّن من مسافات فقط. أمّا if (!trimmed) return; داخل المعالج فحارس ثانٍ تحسّبًا لإرسال النموذج بطريقة أخرى.

أنواع أخرى من الحقول

كل أنواع الحقول تتبع النمط نفسه مع فروق صغيرة:

JSX
// Numbers arrive as strings. Keep the string in state, convert on use.
<input type="number" value={year} onChange={(e) => setYear(e.target.value)} />
const yearNumber = year === '' ? null : Number(year);

// Checkboxes use checked, not value.
<input type="checkbox" checked={watched} onChange={(e) => setWatched(e.target.checked)} />

// A select is controlled through value on the <select>.
<select value={genre} onChange={(e) => setGenre(e.target.value)}>
  <option value="drama">Drama</option>
  <option value="sci-fi">Sci-fi</option>
</select>

حالة الأرقام توقع الجميع مرة على الأقل: e.target.value نص دائمًا، حتى مع type="number"، فيعطي year + 1 النتيجة “20161”. والاحتفاظ بالنص الخام في الـ state يعني أيضًا أن الحقل الفارغ يبقى فارغًا بدل أن يتحوّل إلى 0.

React 19: الـ form actions

الـ controlled inputs هي الأفضل حين تتفاعل مع كل ضغطة مفتاح. لكن نماذج كثيرة لا تهمّ إلا لحظة الإرسال، وقد أضاف React 19 دعمًا مباشرًا لها: مرّر دالة إلى الخاصية action في النموذج.

JSX
function QuickAdd({ onAdd }) {
  function addAction(formData) {
    onAdd(formData.get('title').trim());
  }

  return (
    <form action={addAction}>
      <input name="title" required />
      <button type="submit">Add</button>
    </form>
  );
}

تمنع React إعادة تحميل الصفحة نيابةً عنك، وتستدعي دالتك مع كائن FormData مبني من خصائص name في الحقول، وتعيد ضبط الحقول غير المتحكَّم بها حين تنتهي الـ action. فالحقول ليست متحكَّمًا بها أصلًا.

وحين تحتاج إلى نتيجة من الإرسال، مثل رسالة خطأ، أو إلى حالة انتظار أثناء عمل غير متزامن، أضف useActionState:

JSX
import { useActionState } from 'react';

function QuickAdd({ onAdd }) {
  const [error, formAction, isPending] = useActionState(async (prevError, formData) => {
    const title = formData.get('title').trim();
    if (!title) return 'Title is required';
    await onAdd(title); // could be a request to a server
    return null;
  }, null);

  return (
    <form action={formAction}>
      <input name="title" aria-invalid={error ? true : undefined} />
      <button type="submit" disabled={isPending}>{isPending ? 'Adding…' : 'Add'}</button>
      {error && <p role="alert">{error}</p>}
    </form>
  );
}

تستقبل الـ action الـ state السابق وبيانات النموذج، وتُرجع الـ state التالي. وتكون isPending قيمتها true أثناء تنفيذها. ويستطيع أي component ابن داخل النموذج أيضًا قراءة حالة الانتظار باستخدام useFormStatus من react-dom.

أيّهما تستخدم؟ خيار افتراضي معقول: الـ controlled inputs حين تتفاعل الواجهة أثناء الكتابة (العدّ، والتحقّق المباشر، والحقول المعتمدة على بعضها)؛ والـ actions حين يحدث العمل عند الإرسال، خصوصًا إن كان غير متزامن. نموذج الإضافة في Watchlist يعطّل زرّه ما دام العنوان فارغًا، لذا يستخدم controlled input، وهذا ما ستبنيه في التمرين.

في الدرس التالي تربط هذا النموذج بالقائمة. النموذج والقائمة أخوان، لذا يجب أن تعيش الأفلام في أبيهما المشترك. وهذا هو رفع الـ state للأعلى.

أهم النقاط

  • الـ controlled input يأخذ value من الـ state ويبلّغ عن كل ضغطة مفتاح عبر onChange، فيكون الـ state المصدر الوحيد للحقيقة.
  • تعامل مع onSubmit على الـ <form> واستدعِ e.preventDefault()؛ فتحصل على الإرسال بـ Enter وعلى سلوك يدعم إمكانية الوصول مجانًا.
  • تحقّق من المدخلات انطلاقًا من الـ state أثناء الـ render، مثلًا بتعطيل الزر حين يكون العنوان بعد التشذيب فارغًا.
  • قيم الحقول نصوص دائمًا؛ حوّل الأرقام بنفسك، واستخدم checked لمربّعات الاختيار.
  • تمرّر الـ form actions في React 19 كائن FormData إلى دالة وتعيد ضبط النموذج؛ ويضيف useActionState نتيجة ومؤشّر انتظار.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

ترسم <input value={title} /> من دون onChange. ماذا يحدث حين يكتب المستخدم؟

السؤال 2 من 4

حقل السنة هو <input type="number" value={year} onChange={(e) => setYear(e.target.value)} />. كتب المستخدم 2016. ما نوع year، وماذا يعطي year + 1؟

السؤال 3 من 4

يضيف نموذجك فيلمًا ويجب أن يمسح العنوان بعد ذلك. أيّ handleSubmit صحيح مع controlled input؟

السؤال 4 من 4

متى تكون الـ form action في React 19 مع useActionState أنسب من الـ controlled inputs؟

تدريب

بيئة تجريب الكود

نموذج إضافة الأفلام

ابنِ AddMovieForm بحيث يضيف أفلامًا إلى القائمة. أبقِ المعرّفين title وyear على الحقلين.

  • اجعل الحقلين كليهما controlled (value + onChange).
  • عند الإرسال، استدعِ onAdd(title, year) بالعنوان بعد التشذيب وبالسنة كرقم، أو null حين يكون حقل السنة فارغًا. لا تدع الصفحة تُعاد تحميلها.
  • بعد الإضافة، امسح الحقلين كليهما.
  • يكون زر Add معطّلًا (disabled) ما دام العنوان بعد التشذيب فارغًا.

يخزّن App الأفلام مسبقًا ويعرضها بالشكل “Title (Year)”، أو “Title” وحده حين لا توجد سنة.

import { useState } from 'react';

function AddMovieForm({ onAdd }) {
  // Make both inputs controlled, handle submit, and disable Add while the title is blank.
  return (
    <form>
      <label htmlFor="title">Title</label>
      <input id="title" />
      <label htmlFor="year">Year</label>
      <input id="year" type="number" />
      <button type="submit">Add</button>
    </form>
  );
}

let nextId = 1; // in a real app: crypto.randomUUID()

export default function App() {
  const [movies, setMovies] = useState([]);

  function addMovie(title, year) {
    setMovies([...movies, { id: nextId++, title, year }]);
  }

  return (
    <main>
      <AddMovieForm onAdd={addMovie} />
      <ul>
        {movies.map((m) => (
          <li key={m.id}>{m.year ? `${m.title} (${m.year})` : m.title}</li>
        ))}
      </ul>
    </main>
  );
}
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

المعاينة

الـ console

(لا مخرجات)

الفحوص

  • زر Add معطّل حين يكون العنوان فارغًا أو مسافات فقط
  • يصبح زر Add مفعّلًا بمجرّد كتابة عنوان
  • الإرسال يضيف “Dune (2021)” إلى القائمة
  • يُمسح الحقلان كلاهما بعد الإضافة
  • يُشذَّب العنوان المحاط بمسافات، والسنة الفارغة تعرض العنوان وحده