React: النماذج والمكونات الخاضعة للرقابة
آخر تحديث: 2026-10-26
تُعد النماذج بمثابة جسر بين تطبيقات الويب والمستخدمين. في React، لم تعد النماذج تقتصر على «ملئها وإرسالها» فحسب — بل «تتعرف React على كل حرف تكتبه». ويُطلق على هذا النمط اسم المكون الخاضع للتحكم.
1. ما ستتعلمه
- المكونات الخاضعة للرقابة مقابل المكونات غير الخاضعة للرقابة
- النهج المنظم لاستخدام عناصر input و textarea و select و checkbox و radio
- عملية إرسال النموذج
- التحقق الأساسي من صحة النماذج
- ربطات النماذج المخصصة
2. تطور نموذج التسجيل
(1) المشكلة: صعوبة تتبع البيانات الواردة من النماذج غير الخاضعة للرقابة
أنشأت أليس نموذجًا لتسجيل المستخدمين يسترد البيانات باستخدام الطرق الأصلية:
<!-- HTML Form:Read data all at once upon submission -->
<form id="register-form">
<input name="username" id="username" />
<input type="email" name="email" id="email" />
<input type="password" name="password" id="password" />
<select name="role" id="role">
<option value="developer">Developer</option>
<option value="designer">Designer</option>
</select>
<button type="submit">Register</button>
</form>
<script>
document.getElementById('register-form').addEventListener('submit', function(e) {
e.preventDefault()
// عند الإرسال، اقرأ قيم النموذج
const username = document.getElementById('username').value
const email = document.getElementById('email').value
const password = document.getElementById('password').value
const role = document.getElementById('role').value
// ... Manually verify each field
})
</script>
السؤال:
- عدم الشفافية: ما الذي يتم إدخاله؟ القيمة الوسيطة؟ لا تعرف React ذلك.
- صعوبة التحقق من صحة البيانات في الوقت الفعلي: يتعين عليك مراقبة أحداث الإدخال وتنفيذ منطق التحقق من الصحة يدويًّا.
- مشكلة في ربط بيانات النماذج: يجب برمجة ميزات مثل «ملء رمز المنطقة تلقائيًا بعد اختيار البلد» يدويًّا
(2) حلول للمكونات التي يتم التحكم فيها بواسطة React
▶ جرّب الكودالمزايا: يتتبع React كل حقل إدخال في الوقت الفعلي، مما يوفر التحقق من صحة البيانات ومزامنة البيانات في الوقت الفعلي، ويسترد البيانات الكاملة فور إرسالها.
3. المكونات الخاضعة للرقابة مقابل المكونات غير الخاضعة للرقابة
graph TB
subgraph "Controlled Components(Recommendations)"
A1[User Input] --> B1[onChange Event]
B1 --> C1[Update State]
C1 --> D1[React Render Again]
D1 --> E1[value = State]
end
subgraph "Uncontrolled Components"
A2[User Input] --> B2[Update Directly DOM]
B2 --> C2[Use when submitting ref Reading]
end
style C1 fill:#52c41a,color:#fff
style B2 fill:#faad14,color:#000
| البعد | المكونات الخاضعة للرقابة | المكونات غير الخاضعة للرقابة |
|---|---|---|
| مصدر البيانات | حالة React (المصدر الوحيد للمعلومات) | DOM نفسه |
| طريقة الوصول | القراءة مباشرة من State | يتطلب مرجعًا أو document.getElementById |
| التحقق من الصحة في الوقت الفعلي | ✅ مدعوم أصلاً | ❌ يتطلب مراقبة إضافية |
| المزامنة الفورية | ✅ مزامنة البيانات في الوقت الفعلي | ❌ تتطلب مزامنة يدوية |
| حالات الاستخدام | معظم النماذج | تحميل الملفات، والنماذج البسيطة التي تُملأ مرة واحدة |
القاعدة: استخدم المكونات الخاضعة للرقابة في 90% من الحالات. ويجب أن يستخدم الملف
<input type="file">وحده مكونًا غير خاضع للرقابة.
4. قواعد بناء الجمل الموجهة لعناصر النماذج المختلفة
| عنصر النموذج | الخاصية المرتبطة | قيمة onChange | مثال |
|---|---|---|---|
<input type="text"> |
value |
e.target.value |
<input value={name} onChange={e => setName(e.target.value)} /> |
<input type="number"> |
value |
Number(e.target.value) |
يجب تحويلها إلى قيم رقمية يدويًّا |
<input type="checkbox"> |
checked |
e.target.checked |
قيمة منطقية، ليست قيمة |
<input type="radio"> |
checked |
e.target.value |
مجمعة حسب الاسم؛ تحدد علامة «محدد» ما إذا كانت محددة أم لا |
<textarea> |
value |
e.target.value |
يستخدم React القيمة، وليس نص العنصر التابع |
<select> |
value |
e.target.value |
اختيارات متعددة multiple + مصفوفة |
<input type="file"> |
❌ غير خاضع للتحكم | e.target.files[0] |
لا يمكن استخدامه إلا كخط غير خاضع للتحكم |
(1) إدخال النص
▶ جرّب الكودملاحظة: في React، استخدم الخاصية
<textarea>بدلاً من محتوى النص الفرعي. قيمةe.target.valueالخاصة بـ<input type="number">هي سلسلة نصية؛ لذا تأكد من تحويلها إلى رقم.
(2) مربع الاختيار
▶ جرّب الكود(3) مربعات الاختيار وأزرار الاختيار
▶ جرّب الكودالناتج:
نموذج يحتوي على حقول إدخال ومعالجة للإرسال. مكون React من نوع TypeScript يعرّف props عبر interface
▶ المثال 1: جدول مرجعي سريع لأنواع عناصر النماذج
▶ جرّب الكودالناتج:
مرجع: text (value/onChange)، number (تحويل Number())، email، password، textarea (value)، select (value)، checkbox (checked)، radio (checked)، file (غير مُتحكَّم به، files[0])
5. التحقق من صحة البيانات في النموذج
| توقيت التحقق | الحدث المُحفِّز | المزايا | العيوب | السيناريوهات الموصى بها |
|---|---|---|---|---|
| أثناء الإدخال | onChange | ردود فعل فورية؛ يمكن للمستخدمين تصحيح الأخطاء على الفور | يتم تشغيلها بشكل متكرر؛ يتم الإبلاغ عن الخطأ قبل أن ينتهي المستخدم من الكتابة | قوة كلمة المرور، والتحقق من صحة اسم المستخدم |
| عند فقدان التركيز | onBlur | تقليل عوامل التشتيت؛ لا يتم التحقق من صحة البيانات إلا بعد الانتهاء | لا يرى المستخدمون الأخطاء إلا بعد مغادرة الحقل | معظم حقول النموذج |
| عند الإرسال | onSubmit | المراجعة النهائية والتحقق الموحد | لا يكتشف المستخدم وجود خطأ إلا بعد ملء النموذج | التحقق الاحتياطي النهائي |
(1) التحقق في الوقت الفعلي
function ValidatedForm() {
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
const [errors, setErrors] = React.useState({})
const [touched, setTouched] = React.useState({}) // Record which fields have been accessed
function validate(fieldName, value) {
const newErrors = { ...errors }
if (fieldName === 'email') {
if (!value) newErrors.email = 'The email address cannot be left blank.'
else if (!value.includes('@')) newErrors.email = 'The email address format is incorrect.'
else delete newErrors.email
}
if (fieldName === 'password') {
if (!value) newErrors.password = 'The password cannot be empty.'
else if (value.length < 6) newErrors.password = 'Password must be at least 6 chars'
else if (!/[A-Z]/.test(value)) newErrors.password = 'Passwords must contain uppercase letters'
else delete newErrors.password
}
setErrors(newErrors)
}
function handleBlur(e) {
const { name, value } = e.target
setTouched({ ...touched, [name]: true })
validate(name, value)
}
function handleSubmit(e) {
e.preventDefault()
// Validate all fields before submitting
const allErrors = {}
if (!email || !email.includes('@')) allErrors.email = 'Please enter a valid email address'
if (!password || password.length < 6) allErrors.password = 'Password must be at least 6 chars'
if (Object.keys(allErrors).length > 0) {
setErrors(allErrors)
setTouched({ email: true, password: true })
return
}
alert('Submission Successful!')
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: '400px' }}>
<div style={{ marginBottom: '16px' }}>
<label>Email:</label>
<input
name="email"
type="email"
value={email}
onChange={e => { setEmail(e.target.value); validate('email', e.target.value) }}
onBlur={handleBlur}
style={inputStyle(touched.email && errors.email)}
/>
{touched.email && errors.email && (
<p style={{ color: '#ff4d4f', fontSize: '12px', margin: '4px 0' }}>{errors.email}</p>
)}
</div>
<div style={{ marginBottom: '16px' }}>
<label>Password:</label>
<input
name="password"
type="password"
value={password}
onChange={e => { setPassword(e.target.value); validate('password', e.target.value) }}
onBlur={handleBlur}
style={inputStyle(touched.password && errors.password)}
/>
{touched.password && errors.password && (
<p style={{ color: '#ff4d4f', fontSize: '12px', margin: '4px 0' }}>{errors.password}</p>
)}
</div>
{/* Password Strength Indicator */}
{password && (
<div style={{ marginBottom: '16px' }}>
<p style={{ fontSize: '12px', color: '#666' }}>
Password Strength:
<span style={{ color: password.length < 6 ? '#ff4d4f' : password.length < 10 ? '#faad14' : '#52c41a' }}>
{password.length < 6 ? 'Weak' : password.length < 10 ? 'Medium' : 'Strong'}
</span>
</p>
<div style={{ height: '4px', backgroundColor: '#f0f0f0', borderRadius: '2px' }}>
<div style={{
height: '100%',
width: `${Math.min(100, (password.length / 12) * 100)}%`,
backgroundColor: password.length < 6 ? '#ff4d4f' : password.length < 10 ? '#faad14' : '#52c41a',
borderRadius: '2px',
transition: 'width 0.3s'
}} />
</div>
</div>
)}
<button type="submit" disabled={Object.keys(errors).length > 0}
style={{
width: '100%', padding: '10px',
backgroundColor: Object.keys(errors).length > 0 ? '#d9d9d9' : '#1890ff',
color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer'
}}>
Register
</button>
</form>
)
}
function inputStyle(hasError) {
return {
width: '100%', padding: '8px',
border: `1px solid ${hasError ? '#ff4d4f' : '#d9d9d9'}`,
borderRadius: '4px',
outline: 'none'
}
}
(2) تخصيص الخطاف useForm
افصل منطق النموذج في «هوك» مخصص لتسهيل إعادة استخدامه:
▶ عرض المثال القابل للتشغيل (91 سطرًا)
6. مثال كامل: نموذج تسجيل المستخدم
// ============================================
// Complete Example:User Registration Form
// Features:Controlled Components + Real-Time Verification + Form Data Synchronization
// ============================================
function RegistrationForm() {
const [form, setForm] = React.useState({
username: '',
email: '',
password: '',
confirmPassword: '',
country: '',
gender: '',
acceptTerms: false
})
const [errors, setErrors] = React.useState({})
function validate() {
const newErrors = {}
if (!form.username || form.username.length < 2)
newErrors.username = 'Username must be at least 2 characters'
if (!form.email || !form.email.includes('@'))
newErrors.email = 'Please enter a valid email address'
if (!form.password || form.password.length < 6)
newErrors.password = 'Password must be at least 6 chars'
if (form.password !== form.confirmPassword)
newErrors.confirmPassword = 'The two passwords do not match'
if (!form.country)
newErrors.country = 'Please select a country'
if (!form.acceptTerms)
newErrors.acceptTerms = 'Please agree to the Terms of Service'
return newErrors
}
function handleChange(e) {
const { name, value, type, checked } = e.target
const newValue = type === 'checkbox' ? checked : value
const newForm = { ...form, [name]: newValue }
setForm(newForm)
setErrors(validate())
// Interconnection:Automatically adjust password rule prompts after selecting a country
}
function handleSubmit(e) {
e.preventDefault()
const finalErrors = validate()
setErrors(finalErrors)
if (Object.keys(finalErrors).length === 0) {
alert(`Registration Successful!Welcome ${form.username}`)
}
}
function inputStyle(fieldName) {
return {
width: '100%', padding: '8px', marginBottom: '4px',
border: `1px solid ${errors[fieldName] ? '#ff4d4f' : '#d9d9d9'}`,
borderRadius: '4px'
}
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: '500px', margin: '0 auto' }}>
<h2>📝 User Registration</h2>
<input name="username" value={form.username} onChange={handleChange}
placeholder="Username" style={inputStyle('username')} />
{errors.username && <p style={errorStyle}>{errors.username}</p>}
<input name="email" type="email" value={form.email} onChange={handleChange}
placeholder="Email" style={inputStyle('email')} />
{errors.email && <p style={errorStyle}>{errors.email}</p>}
<input name="password" type="password" value={form.password} onChange={handleChange}
placeholder="Password (at least 6 chars)" style={inputStyle('password')} />
{errors.password && <p style={errorStyle}>{errors.password}</p>}
<input name="confirmPassword" type="password" value={form.confirmPassword}
onChange={handleChange} placeholder="Confirm Password" style={inputStyle('confirmPassword')} />
{errors.confirmPassword && <p style={errorStyle}>{errors.confirmPassword}</p>}
<select name="country" value={form.country} onChange={handleChange}
style={inputStyle('country')}>
<option value="">-- Select a Country --</option>
<option value="CN">China</option>
<option value="US">United States</option>
<option value="JP">Japan</option>
<option value="BR">Brazil</option>
<option value="SA">Saudi Arabia</option>
</select>
{errors.country && <p style={errorStyle}>{errors.country}</p>}
<div style={{ margin: '12px 0' }}>
<p>Gender:</p>
{['Male', 'Female', 'Other'].map(g => (
<label key={g} style={{ marginRight: '16px' }}>
<input type="radio" name="gender" value={g}
checked={form.gender === g} onChange={handleChange} />
{g}
</label>
))}
</div>
<label style={{ display: 'block', margin: '12px 0' }}>
<input type="checkbox" name="acceptTerms" checked={form.acceptTerms}
onChange={handleChange} />
I have read and agree to <a href="#">User Agreement</a>
</label>
{errors.acceptTerms && <p style={errorStyle}>{errors.acceptTerms}</p>}
<button type="submit" style={{
width: '100%', padding: '10px',
backgroundColor: Object.keys(errors).length === 0 && form.username ? '#1890ff' : '#d9d9d9',
color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer'
}}>
Register
</button>
</form>
)
}
const errorStyle = {
color: '#ff4d4f', fontSize: '12px',
margin: '0 0 8px 0'
}
الناتج:
يعرض: "Please select". أنواع الإدخال: text، email، number. القائمة المنسدلة: Please select، Beijing
▶ المثال 2: ربط بيانات النموذج — ملء رموز المناطق تلقائيًا عند اختيار البلد
▶ جرّب الكودالناتج:
منتقي الدولة (US/CN/JP/UK/BR) → تُملأ رمز الدولة تلقائيًا (US: +1، CN: +86). حقل إدخال الهاتف + رمز دولة للقراءة فقط. يعرض "Full: +1 5551234"
الناتج:
يعرض: "Phone Contact". الحالة: form (setter: setForm). الإدخال: Phone number. القائمة المنسدلة: {c}
▶ المثال 3: نموذج ديناميكي — إضافة/حذف أعضاء الفريق
▶ جرّب الكودالناتج:
صفوف أعضاء ديناميكية (Name + Email + قائمة Role منسدلة). "+ Add Member" يضيف صفًا، و"X" يحذف صفًا (مع الإبقاء على صف واحد على الأقل). الإرسال → console.log("Team:", members)
الناتج:
... ('Team:', members)
▶ المثال 4: معالج النماذج متعدد الخطوات
▶ عرض المثال القابل للتشغيل (77 سطرًا)
الناتج:
معالج من 3 خطوات مع شريط تقدم. الخطوة 1: Name+Email، الخطوة 2: Password (6 محارف على الأقل)، الخطوة 3: City+Terms. الخطوة التالية تتحقق من الصحة، ويمكن الرجوع للخلف. الإرسال → "Done!"
الناتج:
يعرض "file upload (uncontrolled)". الحالة: preview، filename. زر: upload. يستخدم useRef
▶ المثال 5: المكونات غير المدارة — تحميل الملفات وuseRef
function FileUploadForm() {
const fileInputRef = useRef(null)
const [preview, setPreview] = useState(null)
const [fileName, setFileName] = useState('')
function handleSubmit(e) {
e.preventDefault()
const file = fileInputRef.current?.files[0]
if (!file) { alert('Please select a file'); return }
console.log('Uploading:', file.name, file.size, 'bytes')
alert(`File "${file.name}" (${(file.size / 1024).toFixed(1)} KB) selected for upload`)
}
function handleFileChange(e) {
const file = e.target.files[0]
if (!file) { setPreview(null); setFileName(''); return }
setFileName(file.name)
if (file.type.startsWith('image/')) {
const reader = new FileReader()
reader.onload = (e) => setPreview(e.target.result)
reader.readAsDataURL(file)
} else {
setPreview(null)
}
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>File Upload (Uncontrolled)</h3>
<div style={{ marginBottom: 12 }}>
<input type="file" ref={fileInputRef} onChange={handleFileChange}
accept="image/*,.pdf,.doc" style={{ marginBottom: 8 }} />
{fileName && <p style={{ fontSize: 13, color: '#666' }}>Selected: {fileName}</p>}
</div>
{preview && (
<div style={{ marginBottom: 12 }}>
<img src={preview} alt="Preview" style={{ maxWidth: '100%', maxHeight: 200, borderRadius: 4, border: '1px solid #eee' }} />
</div>
)}
<button type="submit" style={{ padding: '8px 24px', cursor: 'pointer' }}>Upload</button>
</form>
)
}
الناتج:
إدخال ملف → يعرض معاينة صورة، أو يعرض اسم الملف. عند الرفع، alert "File "photo.jpg" (245.3 KB) selected for upload"
❓ أسئلة شائعة
input[type="checkbox"]، هل يجب أن أستخدم checked أم value؟checked attribute (boolean), not value. To retrieve the new value in the corresponding onChange event, use e.target.checked. Similarly, for radio buttons, use checked.handleChange عندما يحتوي النموذج على العديد من الحقول؟e.target.name بالاقتران مع اسم الخاصية المحسوبة: const { name, value } = e.target; setForm(prev => ({...prev, [name]: value})). وبهذه الطريقة، يمكن لجميع <input name="xxx"> استخدام نفس handleChange. تتطلب خانات الاختيار معالجة خاصة: const newValue = type === 'checkbox' ? checked : value.<input type="file"> لا يمكنك استخدام سوى المكونات غير الخاضعة للرقابة لأن value الخاص بها هو للقراءة فقط. useForm يمكن لـ Hooks تغليف الكود النمطي للمكونات الخاضعة للرقابة.📖 ملخص
- المكونات الخاضعة للتحكم: تخضع قيم عناصر النماذج لحالة React ويتم تحديثها عبر
onChange - 90% من النماذج تستخدم مكونات خاضعة للرقابة؛ ولا يجب استخدام مكون غير خاضع للرقابة إلا في
<input type="file"> - ثلاث لحظات زمنية للتحقق من صحة النموذج: onChange (في الوقت الفعلي)، وonBlur (عند فقدان التركيز)، وonSubmit (عند الإرسال)
- إعادة استخدام منطق معالجة النماذج باستخدام الخصائص العامة
handleChange+name - يمكن لـ«الخطاف» المخصص
useFormأن يضم منطق النموذج لإعادة استخدامه
📝 تمارين
- تمرين أساسي (مستوى الصعوبة ⭐): أنشئ مكونًا باسم
FeedbackFormيتضمن الاسم، والبريد الإلكتروني، والتقييم (خيارات القائمة المنسدلة: إيجابي/محايد/سلبي)، والمحتوى (منطقة نصية). عند الإرسال، اعرض جميع البيانات في نافذة تنبيه. - مشكلة متقدمة (درجة الصعوبة ⭐⭐): قم بإنشاء مكون
PasswordStrengthMeterيعرض قوة كلمة المرور (ضعيفة/متوسطة/قوية) في الوقت الفعلي. القواعد: ≥8 أحرف + أحرف كبيرة وصغيرة + أرقام = قوية؛ ≥6 أحرف = متوسطة؛ بخلاف ذلك = ضعيفة. - التحدي (الصعوبة: ⭐⭐⭐): قم بإنشاء مكون
DynamicFormيتيح للمستخدمين إضافة عناصر إلى النموذج أو حذفها ديناميكيًا (على سبيل المثال، «إضافة عدة أفراد من العائلة»). يجب أن يتضمن كل عنصر في النموذج ثلاثة حقول: الاسم، والعمر، والعلاقة. وعند الإرسال، يجب أن يُخرج المكون مصفوفة.