React: النماذج والمكونات الخاضعة للرقابة
آخر تحديث: 2026-08-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()
// I'll go when I submit it. DOM Data from Zhongna
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
function RegisterForm() {
const [form, setForm] = React.useState({
username: '',
email: '',
password: '',
role: 'developer'
})
// Each input change React Everyone knows
function handleChange(e) {
const { name, value } = e.target
setForm(prev => ({ ...prev, [name]: value }))
}
function handleSubmit(e) {
e.preventDefault()
console.log('Registration Data:', form) // Retrieve the complete form data
}
return (
<form onSubmit={handleSubmit}>
<input name="username" value={form.username} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
<input name="password" value={form.password} onChange={handleChange} />
<select name="role" value={form.role} onChange={handleChange}>
<option value="developer">Developer</option>
<option value="designer">Designer</option>
</select>
<button type="submit">Register</button>
</form>
)
}
المزايا: يتتبع 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) إدخال النص
function TextInputs() {
const [name, setName] = React.useState('')
const [bio, setBio] = React.useState('')
const [age, setAge] = React.useState(18)
return (
<div>
{/* Single-line text */}
<label>Name:</label>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
placeholder="Please enter your name"
/>
{/* Numbers */}
<label>Age:</label>
<input
type="number"
value={age}
onChange={e => setAge(Number(e.target.value))} // Convert to a number!
min={0} max={150}
/>
{/* Multi-line text */}
<label>Personal Profile:</label>
<textarea
value={bio}
onChange={e => setBio(e.target.value)}
rows={4}
placeholder="Introduce Yourself..."
/>
</div>
)
}
ملاحظة: في React، استخدم الخاصية
<textarea>بدلاً من محتوى النص الفرعي. قيمةe.target.valueالخاصة بـ<input type="number">هي سلسلة نصية؛ لذا تأكد من تحويلها إلى رقم.
(2) مربع الاختيار
function SelectInputs() {
const [city, setCity] = React.useState('')
const [hobbies, setHobbies] = React.useState([])
return (
<div>
{/* Single-selection drop-down menu */}
<label>City:</label>
<select value={city} onChange={e => setCity(e.target.value)}>
<option value="">-- Please select --</option>
<option value="beijing">Beijing</option>
<option value="shanghai">Shanghai</option>
<option value="shenzhen">Shenzhen</option>
</select>
{/* Multiple-choice drop-down list */}
<label>Hobbies:</label>
<select
multiple
value={hobbies}
onChange={e => {
const selected = Array.from(e.target.options)
.filter(o => o.selected)
.map(o => o.value)
setHobbies(selected)
}}
>
<option value="coding">Programming</option>
<option value="reading">Read</option>
<option value="gaming">Games</option>
</select>
</div>
)
}
(3) مربعات الاختيار وأزرار الاختيار
function CheckboxAndRadio() {
const [agree, setAgree] = React.useState(false)
const [gender, setGender] = React.useState('')
const [interests, setInterests] = React.useState([])
// Multiple Choice:Toggle Inclusion/Exclusion
function toggleInterest(value) {
setInterests(prev =>
prev.includes(value)
? prev.filter(i => i !== value)
: [...prev, value]
)
}
return (
<div>
{/* Single checkbox */}
<label>
<input
type="checkbox"
checked={agree}
onChange={e => setAgree(e.target.checked)} // Please note that checked No value
/>
Agree to the User Agreement
</label>
{agree && <p style={{ color: '#52c41a' }}>✅ Agreed</p>}
{/* Radio Button Group:The same one name */}
<p>Gender:</p>
{['Male', 'Female', 'Other'].map(g => (
<label key={g} style={{ marginRight: '12px' }}>
<input
type="radio"
name="gender"
value={g}
checked={gender === g}
onChange={e => setGender(e.target.value)}
/>
{g}
</label>
))}
{/* Multiple-Selection Checkbox Group */}
<p>Areas of Interest:</p>
{['Front End', 'Backend', 'AI', 'Design'].map(item => (
<label key={item} style={{ marginRight: '12px' }}>
<input
type="checkbox"
checked={interests.includes(item)}
onChange={() => toggleInterest(item)}
/>
{item}
</label>
))}
</div>
)
}
▶ المثال 1: جدول مرجعي سريع لأنواع عناصر النماذج
// ============================================
// Example:Controlled Syntax for All Common Form Elements
// ============================================
function FormElementReference() {
const [text, setText] = React.useState('')
const [number, setNumber] = React.useState(0)
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
const [bio, setBio] = React.useState('')
const [city, setCity] = React.useState('')
const [agree, setAgree] = React.useState(false)
const [gender, setGender] = React.useState('')
const [file, setFile] = React.useState(null)
return (
<div>
{/* Text Input */}
<input type="text" value={text} onChange={e => setText(e.target.value)} />
{/* Numeric Input */}
<input type="number" value={number} onChange={e => setNumber(Number(e.target.value))} />
{/* Enter your email address */}
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
{/* Password Entry */}
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
{/* Multi-line text */}
<textarea value={bio} onChange={e => setBio(e.target.value)} />
{/* Drop-down menu */}
<select value={city} onChange={e => setCity(e.target.value)}>
<option value="">Please select</option>
<option value="beijing">Beijing</option>
</select>
{/* Single checkbox */}
<input type="checkbox" checked={agree} onChange={e => setAgree(e.target.checked)} />
{/* Radio button */}
<input type="radio" name="gender" value="male" checked={gender === 'male'}
onChange={e => setGender(e.target.value)} />
{/* File Upload(Uncontrolled) */}
<input type="file" onChange={e => setFile(e.target.files[0])} />
{/* Note:File input must be in uncontrolled mode. */}
</div>
)
}
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
افصل منطق النموذج في «هوك» مخصص لتسهيل إعادة استخدامه:
// ============================================
// Example:Custom useForm Hook
// Features:Unified Management of Form Status、Verification、Submit
// ============================================
function useForm(initialValues, validateFn) {
const [values, setValues] = React.useState(initialValues)
const [errors, setErrors] = React.useState({})
const [touched, setTouched] = React.useState({})
// Handling Input Changes
function handleChange(e) {
const { name, value, type, checked } = e.target
const newValue = type === 'checkbox' ? checked : value
const newValues = { ...values, [name]: newValue }
setValues(newValues)
// Real-Time Verification
if (validateFn) {
const newErrors = validateFn(newValues)
setErrors(newErrors)
}
}
// Handling Loss of Focus
function handleBlur(e) {
const { name } = e.target
setTouched({ ...touched, [name]: true })
}
// Process Submission
function handleSubmit(callback) {
return (e) => {
e.preventDefault()
// Mark all fields as checked
const allTouched = Object.keys(values).reduce((acc, key) => {
acc[key] = true
return acc
}, {})
setTouched(allTouched)
// Verification
if (validateFn) {
const newErrors = validateFn(values)
setErrors(newErrors)
if (Object.keys(newErrors).length > 0) return
}
callback(values)
}
}
// Reset Form
function reset() {
setValues(initialValues)
setErrors({})
setTouched({})
}
return { values, errors, touched, handleChange, handleBlur, handleSubmit, reset }
}
// ---- Usage useForm ----
function LoginForm() {
function validate(values) {
const errors = {}
if (!values.email) errors.email = 'Please enter your email address'
else if (!values.email.includes('@')) errors.email = 'The email address format is incorrect.'
if (!values.password) errors.password = 'Please enter your password'
else if (values.password.length < 6) errors.password = 'Password must be at least 6 chars'
return errors
}
const form = useForm({ email: '', password: '' }, validate)
return (
<form onSubmit={form.handleSubmit(data => console.log('Submit:', data))}
style={{ maxWidth: '400px', margin: '0 auto' }}>
<h2>Log In</h2>
<input
name="email"
value={form.values.email}
onChange={form.handleChange}
onBlur={form.handleBlur}
placeholder="Email"
style={{ width: '100%', padding: '8px', marginBottom: '4px', border: `1px solid ${form.touched.email && form.errors.email ? '#ff4d4f' : '#d9d9d9'}`, borderRadius: '4px' }}
/>
{form.touched.email && form.errors.email && (
<p style={{ color: '#ff4d4f', fontSize: '12px', margin: '0 0 12px 0' }}>{form.errors.email}</p>
)}
<input
name="password"
type="password"
value={form.values.password}
onChange={form.handleChange}
onBlur={form.handleBlur}
placeholder="Password"
style={{ width: '100%', padding: '8px', marginBottom: '4px', border: `1px solid ${form.touched.password && form.errors.password ? '#ff4d4f' : '#d9d9d9'}`, borderRadius: '4px' }}
/>
{form.touched.password && form.errors.password && (
<p style={{ color: '#ff4d4f', fontSize: '12px', margin: '0 0 12px 0' }}>{form.errors.password}</p>
)}
<button type="submit" style={{
width: '100%', padding: '10px', backgroundColor: '#1890ff',
color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer'
}}>
Log In
</button>
<button type="button" onClick={form.reset}
style={{ width: '100%', padding: '8px', marginTop: '8px', cursor: 'pointer' }}>
Reset
</button>
</form>
)
}
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'
}
▶ المثال 2: ربط بيانات النموذج — ملء رموز المناطق تلقائيًا عند اختيار البلد
function PhoneForm() {
const [form, setForm] = useState({ country: 'US', phone: '' })
const areaCodes = { US: '+1', CN: '+86', JP: '+81', UK: '+44', BR: '+55' }
function handleCountryChange(e) {
const country = e.target.value
setForm(prev => ({
...prev,
country,
phone: prev.phone.replace(/^\+\d+\s*/, '') + ' ',
}))
}
function handleChange(e) {
const { name, value } = e.target
setForm(prev => ({ ...prev, [name]: value }))
}
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Phone Contact</h3>
<select name="country" value={form.country} onChange={handleCountryChange}
style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }}>
{Object.keys(areaCodes).map(c => (
<option key={c} value={c}>{c}</option>
))}
</select>
<div style={{ display: 'flex', gap: 8 }}>
<input value={areaCodes[form.country]} readOnly style={{ width: 60, padding: 8, borderRadius: 4 }} />
<input name="phone" value={form.phone} onChange={handleChange}
placeholder="Phone number" style={{ flex: 1, padding: 8, borderRadius: 4 }} />
</div>
<p style={{ color: '#666', fontSize: 13, marginTop: 8 }}>
Full: {areaCodes[form.country]} {form.phone.trim()}
</p>
</div>
)
}
▶ المثال 3: نموذج ديناميكي — إضافة/حذف أعضاء الفريق
function TeamForm() {
const [members, setMembers] = useState([{ name: '', email: '', role: 'developer' }])
function addMember() {
setMembers(prev => [...prev, { name: '', email: '', role: 'developer' }])
}
function removeMember(index) {
setMembers(prev => prev.filter((_, i) => i !== index))
}
function updateMember(index, field, value) {
setMembers(prev => prev.map((m, i) => i === index ? { ...m, [field]: value } : m))
}
function handleSubmit(e) {
e.preventDefault()
console.log('Team:', members)
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: 500, margin: '0 auto' }}>
<h3>Team Members</h3>
{members.map((m, i) => (
<div key={i} style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'center' }}>
<input value={m.name} onChange={e => updateMember(i, 'name', e.target.value)}
placeholder="Name" style={{ flex: 1, padding: 6, borderRadius: 4 }} />
<input value={m.email} onChange={e => updateMember(i, 'email', e.target.value)}
placeholder="Email" style={{ flex: 1, padding: 6, borderRadius: 4 }} />
<select value={m.role} onChange={e => updateMember(i, 'role', e.target.value)}
style={{ padding: 6, borderRadius: 4 }}>
<option value="developer">Dev</option>
<option value="designer">Design</option>
<option value="manager">PM</option>
</select>
{members.length > 1 && (
<button type="button" onClick={() => removeMember(i)}
style={{ padding: '6px 10px', color: '#ff4d4f', border: 'none', cursor: 'pointer' }}>
X
</button>
)}
</div>
))}
<button type="button" onClick={addMember}
style={{ padding: '6px 16px', marginRight: 8, cursor: 'pointer' }}>
+ Add Member
</button>
<button type="submit" style={{ padding: '6px 16px', cursor: 'pointer' }}>Submit</button>
</form>
)
}
▶ المثال 4: معالج النماذج متعدد الخطوات
function MultiStepForm() {
const [step, setStep] = useState(1)
const [form, setForm] = useState({ name: '', email: '', password: '', city: '', agree: false })
const [errors, setErrors] = useState({})
function updateField(e) {
const { name, value, type, checked } = e.target
setForm(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value }))
}
function validate() {
const errs = {}
if (step === 1) {
if (!form.name.trim()) errs.name = 'Name required'
if (!form.email.includes('@')) errs.email = 'Valid email required'
} else if (step === 2) {
if (form.password.length < 6) errs.password = 'At least 6 characters'
} else if (step === 3) {
if (!form.city) errs.city = 'Select a city'
if (!form.agree) errs.agree = 'Must agree to terms'
}
setErrors(errs)
return Object.keys(errs).length === 0
}
function next() { if (validate()) setStep(s => Math.min(s + 1, 3)) }
function back() { setStep(s => Math.max(s - 1, 1)) }
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Step {step} of 3</h3>
<div style={{ display: 'flex', gap: 4, marginBottom: 16 }}>
{[1, 2, 3].map(s => (
<div key={s} style={{ flex: 1, height: 4, borderRadius: 2,
backgroundColor: s <= step ? '#1890ff' : '#f0f0f0' }} />
))}
</div>
{step === 1 && (
<>
<input name="name" value={form.name} onChange={updateField} placeholder="Name"
style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4, border: errors.name ? '1px solid #ff4d4f' : undefined }} />
{errors.name && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.name}</p>}
<input name="email" value={form.email} onChange={updateField} placeholder="Email"
style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4, border: errors.email ? '1px solid #ff4d4f' : undefined }} />
{errors.email && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.email}</p>}
</>
)}
{step === 2 && (
<>
<input name="password" type="password" value={form.password} onChange={updateField}
placeholder="Password" style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4 }} />
{errors.password && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.password}</p>}
</>
)}
{step === 3 && (
<>
<select name="city" value={form.city} onChange={updateField}
style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4 }}>
<option value="">Select city</option>
<option value="beijing">Beijing</option>
<option value="shanghai">Shanghai</option>
</select>
{errors.city && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.city}</p>}
<label style={{ display: 'block', marginTop: 8 }}>
<input type="checkbox" name="agree" checked={form.agree} onChange={updateField} />
I agree to the terms
</label>
{errors.agree && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.agree}</p>}
</>
)}
<div style={{ marginTop: 16 }}>
{step > 1 && <button type="button" onClick={back} style={{ marginRight: 8, cursor: 'pointer' }}>Back</button>}
{step < 3 ? (
<button type="button" onClick={next} style={{ cursor: 'pointer' }}>Next</button>
) : (
<button type="button" onClick={() => { if (validate()) alert('Done!') }} style={{ cursor: 'pointer' }}>Submit</button>
)}
</div>
</div>
)
}
▶ المثال 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>
)
}
❓ أسئلة شائعة
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يتيح للمستخدمين إضافة عناصر إلى النموذج أو حذفها ديناميكيًا (على سبيل المثال، «إضافة عدة أفراد من العائلة»). يجب أن يتضمن كل عنصر في النموذج ثلاثة حقول: الاسم، والعمر، والعلاقة. وعند الإرسال، يجب أن يُخرج المكون مصفوفة.