عرض القوائم في React
في React، ستحتاج غالباً إلى عرض مكونات متعددة متشابهة من مجموعة بيانات. الطريقة الأكثر شيوعاً للقيام بذلك هي استخدام دالة JavaScript map() لتحويل مصفوفة من البيانات إلى مصفوفة من المكونات.
المفهوم الأساسي: عند عرض القوائم في React، يحتاج كل عنصر إلى prop فريد يسمى "key". تساعد المفاتيح React على تحديد العناصر التي تغيرت أو أُضيفت أو أُزيلت، مما يتيح تحديثات فعالة لـ DOM.
عرض القائمة الأساسي مع map()
تنشئ دالة map() مصفوفة جديدة عن طريق استدعاء دالة على كل عنصر في المصفوفة الأصلية. في React، نستخدمها لتحويل البيانات إلى عناصر JSX:
function NumberList() {
const numbers = [1, 2, 3, 4, 5];
return (
<ul>
{numbers.map((number) => (
<li key={number}>{number}</li>
))}
</ul>
);
}
// مع الكائنات
function UserList() {
const users = [
{ id: 1, name: 'علي', age: 25 },
{ id: 2, name: 'محمد', age: 30 },
{ id: 3, name: 'فاطمة', age: 35 }
];
return (
<div>
{users.map((user) => (
<div key={user.id} className="user-card">
<h3>{user.name}</h3>
<p>العمر: {user.age}</p>
</div>
))}
</div>
);
}
فهم المفاتيح
المفاتيح هي سمات خاصة تساعد React على تحديد العناصر في القائمة التي تغيرت. يجب أن تكون مستقرة وقابلة للتنبؤ وفريدة بين الأشقاء.
function TodoList() {
const todos = [
{ id: 'todo-1', text: 'تعلم React', completed: false },
{ id: 'todo-2', text: 'بناء مشروع', completed: false },
{ id: 'todo-3', text: 'النشر إلى الإنتاج', completed: true }
];
return (
<ul>
{todos.map((todo) => (
<li
key={todo.id}
style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}
>
{todo.text}
</li>
))}
</ul>
);
}
أفضل ممارسة: استخدم معرفات فريدة ومستقرة (مثل معرفات قاعدة البيانات) كمفاتيح. يجب ألا تتغير المفاتيح بين العروض - يجب أن تكون متأصلة في البيانات نفسها، وليس مُولدة أثناء التنفيذ.
لماذا المفاتيح مهمة
تخبر المفاتيح React بعنصر المصفوفة الذي يتوافق مع كل مكون، حتى يتمكن من مطابقتها لاحقاً. هذا مهم إذا كان يمكن نقل عناصر المصفوفة أو حذفها أو إدراج عناصر فيها.
// سيء: بدون مفاتيح، لا يمكن لـ React تتبع العناصر بشكل صحيح
function BadList({ items }) {
return (
<ul>
{items.map((item) => (
<li>{item.name}</li> {/* مفتاح مفقود! */}
))}
</ul>
);
}
// جيد: مع مفاتيح صحيحة
function GoodList({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
استخراج المكونات مع المفاتيح
عند استخراج مكون من قائمة، احتفظ بالمفتاح في المكون في استدعاء map()، وليس في المكون نفسه:
// تعريف المكون - لا مفتاح هنا
function ListItem({ todo }) {
return (
<li>
<strong>{todo.title}</strong>: {todo.description}
</li>
);
}
// المكون الأب - المفتاح يذهب هنا
function TodoList() {
const todos = [
{ id: 1, title: 'التسوق', description: 'شراء البقالة' },
{ id: 2, title: 'الدراسة', description: 'قراءة توثيق React' },
{ id: 3, title: 'التمرين', description: 'الذهاب للجري' }
];
return (
<ul>
{todos.map((todo) => (
<ListItem key={todo.id} todo={todo} />
))}
</ul>
);
}
خطأ شائع: لا تستخدم أبداً فهرس المصفوفة كمفتاح إذا كان يمكن إعادة ترتيب القائمة أو تصفيتها أو إضافة/إزالة عناصر منها. استخدام الفهرس كمفتاح يمكن أن يسبب أخطاء دقيقة ومشاكل في الأداء. استخدم الفهرس فقط كملاذ أخير للقوائم الثابتة التي لا تتغير أبداً.
الفهرس كمفتاح - متى يكون مقبولاً
يكون استخدام فهرس المصفوفة كمفتاح مقبولاً فقط في هذه الحالات المحددة:
// مقبول: قائمة ثابتة لا تتغير أبداً
function MonthList() {
const months = [
'يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو',
'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'
];
return (
<ul>
{months.map((month, index) => (
<li key={index}>{month}</li>
))}
</ul>
);
}
// سيء: قائمة ديناميكية - يمكن إعادة ترتيب العناصر
function DynamicList({ items }) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item.name}</li> {/* مشكلة! */}
))}
</ul>
);
}
// جيد: استخدم معرفات فريدة للقوائم الديناميكية
function DynamicListFixed({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
توليد معرفات فريدة
إذا لم تحتوي بياناتك على معرفات فريدة، يمكنك إنشاؤها. إليك بعض الأساليب:
import { useState } from 'react';
function TodoApp() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
// الطريقة 1: استخدام Date.now() + عشوائي
const addTodo1 = () => {
const newTodo = {
id: Date.now() + Math.random(), // فريد بما فيه الكفاية لمعظم الحالات
text: input
};
setTodos([...todos, newTodo]);
setInput('');
};
// الطريقة 2: استخدام crypto.randomUUID() (المتصفحات الحديثة)
const addTodo2 = () => {
const newTodo = {
id: crypto.randomUUID(), // دالة متصفح أصلية
text: input
};
setTodos([...todos, newTodo]);
setInput('');
};
// الطريقة 3: معرفات قائمة على العداد
let nextId = 0;
const addTodo3 = () => {
const newTodo = {
id: `todo-${nextId++}`, // معرف تزايدي بسيط
text: input
};
setTodos([...todos, newTodo]);
setInput('');
};
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button onClick={addTodo1}>إضافة</button>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
القوائم المتداخلة
يمكنك الحصول على قوائم داخل قوائم. كل مستوى يحتاج إلى مفاتيحه الفريدة:
function CategoryList() {
const categories = [
{
id: 'cat-1',
name: 'الفواكه',
items: [
{ id: 'item-1', name: 'تفاح' },
{ id: 'item-2', name: 'موز' },
{ id: 'item-3', name: 'برتقال' }
]
},
{
id: 'cat-2',
name: 'الخضروات',
items: [
{ id: 'item-4', name: 'جزر' },
{ id: 'item-5', name: 'بروكلي' },
{ id: 'item-6', name: 'سبانخ' }
]
}
];
return (
<div>
{categories.map((category) => (
<div key={category.id} className="category">
<h2>{category.name}</h2>
<ul>
{category.items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
))}
</div>
);
}
نطاق المفتاح: تحتاج المفاتيح فقط إلى أن تكون فريدة بين الأشقاء، وليس عالمياً. يمكن لقائمتين مختلفتين استخدام نفس المفاتيح دون تعارض.
تصفية وتحويل القوائم
يمكنك دمج filter() و map() ودوال المصفوفة الأخرى للتلاعب بالقوائم قبل العرض:
function ProductList() {
const products = [
{ id: 1, name: 'لابتوب', price: 999, inStock: true },
{ id: 2, name: 'ماوس', price: 25, inStock: true },
{ id: 3, name: 'لوحة مفاتيح', price: 75, inStock: false },
{ id: 4, name: 'شاشة', price: 299, inStock: true },
{ id: 5, name: 'كاميرا ويب', price: 89, inStock: false }
];
// تصفية المنتجات غير المتوفرة
const inStockProducts = products.filter(product => product.inStock);
// الترتيب حسب السعر
const sortedProducts = [...products].sort((a, b) => a.price - b.price);
// الحصول على العناصر الباهظة الثمن فقط (>$100)
const expensiveProducts = products.filter(product => product.price > 100);
return (
<div>
<h2>المنتجات المتوفرة</h2>
<ul>
{inStockProducts.map(product => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
<h2>مرتبة حسب السعر</h2>
<ul>
{sortedProducts.map(product => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
<h2>المنتجات الفاخرة</h2>
<ul>
{expensiveProducts.map(product => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
</div>
);
}
معالجة القوائم الفارغة
فكر دائماً في ما يجب عرضه عندما تكون القائمة فارغة:
function MessageList({ messages }) {
return (
<div>
<h2>الرسائل</h2>
{messages.length === 0 ? (
<p className="empty-state">لا توجد رسائل بعد. ابدأ محادثة!</p>
) : (
<ul>
{messages.map((message) => (
<li key={message.id}>
<strong>{message.sender}:</strong> {message.text}
</li>
))}
</ul>
)}
</div>
);
}
تجربة المستخدم: قدم دائماً ملاحظات مفيدة عندما تكون القوائم فارغة. الحالات الفارغة هي فرصة لتوجيه المستخدمين حول ما يجب فعله بعد ذلك أو شرح سبب فراغ القائمة.
تمرين 1: قائمة درجات الطلاب
أنشئ مكوناً يعرض قائمة الطلاب مع درجاتهم:
- مصفوفة من كائنات الطلاب: { id, name, grade, passed }
- عرض اسم ودرجة كل طالب
- إظهار "نجح" باللون الأخضر أو "رسب" باللون الأحمر بناءً على حالة النجاح
- إضافة مرشح لإظهار الطلاب الناجحين فقط أو الراسبين فقط
- إضافة زر فرز للترتيب حسب الدرجة (من الأعلى إلى الأدنى)
- عرض إجمالي عدد الطلاب ونسبة النجاح
تلميح: استخدم filter() و sort() قبل map().
تمرين 2: سلسلة تعليقات متداخلة
ابنِ نظام تعليقات مع ردود متداخلة:
- كل تعليق يحتوي على: id، author، text، و replies (مصفوفة من كائنات التعليق)
- عرض التعليقات مع ردودها مسافة بادئة أدناه
- يمكن أن يكون للردود ردودها الخاصة (هيكل تكراري)
- إضافة زر "رد" لكل تعليق (يمكن استخدام alert الآن)
- إظهار عدد التعليقات لكل سلسلة
- تنسيق مختلف بناءً على مستوى التداخل
إضافي: أنشئ مكوناً تكرارياً يمكنه التعامل مع أي عمق تداخل.
تمرين 3: كتالوج منتجات التجارة الإلكترونية
أنشئ كتالوج منتجات بخيارات تصفية وفرز متعددة:
- مصفوفة منتجات مع: id، name، price، category، rating، inStock
- عرض المنتجات في تخطيط شبكي
- التصفية حسب الفئة (إلكترونيات، ملابس، كتب، إلخ.)
- التصفية حسب التوفر (متوفر / غير متوفر)
- الفرز حسب: السعر (من الأدنى إلى الأعلى / من الأعلى إلى الأدنى)، التقييم، الاسم (أ-ي)
- إظهار تراكب "نفذت الكمية" على المنتجات غير المتاحة
- عرض إجمالي عدد المنتجات ومتوسط سعر النتائج المصفاة
تلميح: اربط عمليات filter() و sort() المتعددة بناءً على اختيارات المستخدم.