الوحدة 3 · الـ State والأحداث
النماذج والـ controlled inputs
24 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
ابنِ نموذج إضافة الأفلام في Watchlist باستخدام الـ controlled inputs، وتعامل مع الإرسال والتحقّق من المدخلات، واعرف متى تكون الـ form actions وuseActionState في React 19 الخيار الأنسب.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
كل تطبيق مفيد يستقبل مدخلات. في Watchlist يعني ذلك نموذجًا: اكتب عنوانًا، وسنة إن أردت، واضغط Enter أو انقر Add، فيظهر الفيلم في القائمة. النماذج هي المكان الذي يلتقي فيه الـ state بالأحداث، وفيها يعيش معظم أخطاء المبتدئين: حقول ترفض الكتابة، وصفحات يُعاد تحميلها عند الإرسال، وأرقام تُلصق ببعضها كالنصوص. ولكل واحد منها سبب واضح.
الـ controlled input
في HTML العادي يملك الحقل قيمته، وأنت تقرؤها حين تحتاجها. أمّا في React فالأسلوب المعتاد يقلب ذلك: الـ state يملك القيمة، والحقل يعرضها.
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، لا فقط حين يفقد الحقل التركيز.
الإرسال
تعامل مع الإرسال على النموذج، لا على نقرة الزر:
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; داخل المعالج فحارس ثانٍ تحسّبًا لإرسال النموذج بطريقة أخرى.
أنواع أخرى من الحقول
كل أنواع الحقول تتبع النمط نفسه مع فروق صغيرة:
// 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 في النموذج.
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:
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نتيجة ومؤشّر انتظار.