التعامل مع الكائنات والمصفوفات داخل useState وحفظ مبدأ Immutability
| المفهوم الهندسي | الآلية البرمجية في React | النتيجة والتطبيق المباشر |
|---|---|---|
| Immutability | إنشاء نسخة جديدة من الكائن أو المصفوفة بدلاً من تعديل الأصل مباشرة | ضمان اكتشاف ريأكت لتغير المرجع في الذاكرة وإعادة رسم الواجهة بدقة |
| Spread Operator (...) | نسخ كافة عناصر الكائن أو المصفوفة القديمة في هيكل جديد بسهولة | تعديل الخصائص المراد تغييرها فقط مع الإبقاء على باقي البيانات دون تلف |
| تحديث المصفوفات بحذر | استخدام دوال نقية مثل map و filter و concat بدلاً من push و splice | إمكانية إضافة أو حذف أو تعديل عناصر القوائم بأمان تام دون المساس بالمصفوفة الأصلية |
| Nested Updates | نسخ كل مستوى من مستويات الكائنات المتداخلة باستخدام النشر المتعدد | منع المشاكل الخفية الناتجة عن مشاركة المراجع للبيانات المتداخلة |
عندما تكون القيمة المخزنة في useState نصاً أو رقماً بسيطاً (Primitive Values)، يكون التحديث مستقيماً. ولكن عندما تتعامل مع كائنات (Objects) أو مصفوفات (Arrays)، تختلف قواعد اللعبة تماماً!
في جافا سكريبت، الكائنات والمصفوفات تُخزن عن طريق المرجع في الذاكرة (Reference) وليس القيمة.
إذا قمت بتعديل خاصية داخل كائن مباشرة هكذا: user.name = "أحمد"، فإن المرجع الخص بالكائن في الذاكرة لم يتغير. وعندما تبعث هذا الكائن المكتوب عليه إلى دالة setUser(user)، ستقارن ريأكت المرجع القديم بالمرجع الجديد، وستجدهما متطابقين تماماً! والنتيجة؟ لن تقوم ريأكت بإعادة رندرة الشاشة إطلاقاً!
لكي تشعر ريأكت بالتغير، يجب أن تعطيها كائناً جديداً تماماً بموقع ذاكرة جديد. ونحقق ذلك بفضل معامل النشر (Spread Operator ...).
دعنا نطبق التعامل مع الكائنات والمصفوفات لبناء لوحة ملف الطالب وقائمة كورساته المحددة في منصة نوفيل:
| 1 | import { useState } from 'react'; |
| 2 | |
| 3 | function UserProfileManager() { |
| 4 | // 1. حالة على هيئة كائن يحتوي على بيانات الطالب |
| 5 | const [user, setUser] = useState({ |
| 6 | name: 'أحمد محمود', |
| 7 | email: 'ahmed@nouvil.net', |
| 8 | role: 'طالب' |
| 9 | }); |
| 10 | |
| 11 | // 2. حالة على هيئة مصفوفة تحتوي على الكورسات المسجلة |
| 12 | const [enrolledCourses, setEnrolledCourses] = useState([ |
| 13 | { id: 1, title: 'أساسيات HTML & CSS', completed: true }, |
| 14 | { id: 2, title: 'احتراف JavaScript الحديثة', completed: false } |
| 15 | ]); |
| 16 | |
| 17 | // تحديث خاصية داخل الكائن مع الحفاظ على باقي الخصائص |
| 18 | const handleRoleChange = (newRole) => { |
| 19 | setUser((prevUser) => ({ |
| 20 | ...prevUser, // نسخ البيانات القديمة |
| 21 | role: newRole // تعديل الخاصية المطلوبة فقط |
| 22 | })); |
| 23 | }; |
| 24 | |
| 25 | // إضافة عنصر جديد للمصفوفة |
| 26 | const handleAddCourse = () => { |
| 27 | const newCourse = { |
| 28 | id: Date.now(), |
| 29 | title: 'بناء واجهات React متقدمة', |
| 30 | completed: false |
| 31 | }; |
| 32 | // إنشاء مصفوفة جديدة تحتوي القديم + الجديد |
| 33 | setEnrolledCourses((prevCourses) => [...prevCourses, newCourse]); |
| 34 | }; |
| 35 | |
| 36 | // حذف عنصر من المصفوفة باستخدام filter |
| 37 | const handleDeleteCourse = (courseId) => { |
| 38 | setEnrolledCourses((prevCourses) => |
| 39 | prevCourses.filter((course) => course.id !== courseId) |
| 40 | ); |
| 41 | }; |
| 42 | |
| 43 | // تعديل حالة كورس محدد داخل المصفوفة باستخدام map |
| 44 | const handleToggleCourse = (courseId) => { |
| 45 | setEnrolledCourses((prevCourses) => |
| 46 | prevCourses.map((course) => { |
| 47 | if (course.id === courseId) { |
| 48 | // إنتاج كائن جديد للكورس المعدل |
| 49 | return { ...course, completed: !course.completed }; |
| 50 | } |
| 51 | return course; // إرجاع باقي العناصر كما هي |
| 52 | }) |
| 53 | ); |
| 54 | }; |
| 55 | |
| 56 | return ( |
| 57 | <div style={{ padding: '20px', fontFamily: 'sans-serif', border: '2px solid #673ab7', borderRadius: '8px' }}> |
| 58 | <h2>تحديث البيانات المركبة (Objects & Arrays)</h2> |
| 59 | |
| 60 | {/* عرض بيانات الكائن */} |
| 61 | <div style={{ backgroundColor: '#f5f5f5', padding: '15px', borderRadius: '5px' }}> |
| 62 | <h3>بيانات الملف الشخصي:</h3> |
| 63 | <p>الاسم: {user.name}</p> |
| 64 | <p>البريد: {user.email}</p> |
| 65 | <p>الرتبة الحالية: <strong>{user.role}</strong></p> |
| 66 | <button onClick={() => handleRoleChange('مطور متقدم')} style={{ padding: '5px 10px', cursor: 'pointer' }}> |
| 67 | رقّي الحساب إلى مطور متقدم |
| 68 | </button> |
| 69 | </div> |
| 70 | |
| 71 | <hr style={{ margin: '20px 0' }} /> |
| 72 | |
| 73 | {/* عرض وتحديث المصفوفة */} |
| 74 | <div> |
| 75 | <h3>الكورسات المسجلة ({enrolledCourses.length}):</h3> |
| 76 | <button onClick={handleAddCourse} style={{ backgroundColor: '#4caf50', color: '#fff', border: 'none', padding: '8px 12px', border: 'none', borderRadius: '4px', cursor: 'pointer', marginBottom: '10px' }}> |
| 77 | + إضافة كورس جديد |
| 78 | </button> |
| 79 | |
| 80 | <ul> |
| 81 | {enrolledCourses.map((course) => ( |
| 82 | <li key={course.id} style={{ marginBottom: '8px' }}> |
| 83 | <span style={{ textDecoration: course.completed ? 'line-through' : 'none', marginLeft: '10px' }}> |
| 84 | {course.title} |
| 85 | </span> |
| 86 | <button onClick={() => handleToggleCourse(course.id)} style={{ marginLeft: '5px' }}> |
| 87 | {course.completed ? 'إلغاء الإكمال' : 'تحديد كـ مكتمل'} |
| 88 | </button> |
| 89 | <button onClick={() => handleDeleteCourse(course.id)} style={{ backgroundColor: '#f44336', color: '#fff', border: 'none', padding: '2px 8px', borderRadius: '3px', cursor: 'pointer' }}> |
| 90 | حذف |
| 91 | </button> |
| 92 | </li> |
| 93 | ))} |
| 94 | </ul> |
| 95 | </div> |
| 96 | </div> |
| 97 | ); |
| 98 | } |
| 99 | |
| 100 | export default UserProfileManager; |
ℹ️ معلومة
تذكر أن المعامل النقطي (...Spread Operator) يقوم بـ نسخ سطحي (Shallow Copy) فقط! إذا كان الكائن يحتوي على كائن آخر بداخله:
| 1 | const [student, setStudent] = useState({ |
| 2 | name: 'أحمد', |
| 3 | details: { age: 22, city: 'القاهرة' } |
| 4 | }); |
| 1 | const [student, setStudent] = useState({ |
| 2 | name: 'أحمد', |
| 3 | details: { age: 22, city: 'القاهرة' } |
| 4 | }); |
لو قمت بكتابة: setStudent({ ...student, details: { city: 'الإسكندرية' } }) ستفقد الخاصية age تماماً لأنك استبدلت كائن details بالكامل! الحل الصحيح للكائنات المتداخلة: يجب عليك نسخ المستوى الخارجي ثم نسخ المستوى الداخلي بالتتابع:
| 1 | // ✅ التحديث الآمن للكائنات المتداخلة |
| 2 | setStudent((prev) => ({ |
| 3 | ...prev, |
| 4 | details: { |
| 5 | ...prev.details, // حفظ age |
| 6 | city: 'الإسكندرية' // تعديل city فقط |
| 7 | } |
| 8 | })); |