معالجة البيانات والقوائم المركبة (Sorting, Filtering, Pagination)
| العملية الهندسية | الآلية البرمجية في React | الأثر على الأداء وتجربة المستخدم (UX) |
|---|---|---|
| الفلترة الحية Filtering | استخدام دالة filter مع ربط القيمة بحالة البحث (Search State) | تصفية القائمة لحظياً وعرض النتائج المطابقة فقط دون الاتصال بالسيرفر في كل حرف |
| الترتيب الديناميكي Sorting | استخدام دالة sort مع نسخ المصفوفة لمنع تعديل الأصل (Immutability) | ترتيب العناصر أبجدياً أو حسب السعر/التاريخ دون إعطاب مراجع الذاكرة |
| التقسيم إلى صفحات Pagination | استقطاع جزء محدد من البيانات باستخدام slice بناءً على رقم الصفحة | تخفيف العبء عن شجرة الـ DOM وتوفير واجهة مريحة ومنظمة |
| الحسابات المحسّنة Memoized Calculations | تغليف عمليات المعالجة المركبة بـ useMemo | منع إعادة تنفيذ الترتيب والفلترة المجهدة عند رندرة المكون لأسباب غير متعلقة بالبيانات |
عند بناء لوحات التحكم أو القوائم التفاعلية، لا يُكتفى بعرض البيانات كما وردت من السيرفر. يرغب المستخدم دائماً في تقليص البيانات وتخصيص طريقة عرضها.
الهدف الهندسي هنا هو إجراء الجمع بين الفلترة، الترتيب، والتقسيم بأسلوب سلس يضمن أقصى أداء ممكن دون التأثير على ذاكرة التطبيق.
ℹ️ معلومة
الفلترة هي استبعاد العناصر التي لا تطابق كلمة البحث التي يدخلها المستخدم.
يتم التحقق من النصوص بدون الحساسية لحالة الأحرف (Case-Insensitive) عبر تحويل الطرفين إلى toLowerCase().
| 1 | // فلترة العناصر التي تحتوي على كلمة البحث داخل عنوان الكورس |
| 2 | const filteredCourses = courses.filter((course) => |
| 3 | course.title.toLowerCase().includes(searchQuery.toLowerCase()) |
| 4 | ); |
بعد الفلترة، تأتي مرحلة الترتيب بناءً على معيار معين (السعر، الأبجدية، التاريخ).
ℹ️ معلومة
دالة .sort() التابعة لجافا سكريبت تعدل المصفوفة الأصلية مباشرة (Mutation). هذا يكسر مبدأ الـ Immutability في React ويميل إلى منع إعادة الرندرة بالشكل الصحيح!
إنشاء نسخة
| 1 | // خطأ: تعديل الأصل يفسد الـ State |
| 2 | // courses.sort(...); |
| 3 | |
| 4 | // صح: استخدام Spread Operator لإنشاء مصفوفة جديدة قبل الترتيب |
| 5 | const sortedCourses = [...filteredCourses].sort((a, b) => { |
| 6 | if (sortBy === 'price') return a.price - b.price; // ترتيب تصاعدي حسب السعر |
| 7 | return a.title.localeCompare(b.title); // ترتيب أبجدي صحيح للكلمات |
| 8 | }); |
بدلاً من عرض 100 عنصر في الشاشة وإغراق الـ DOM، نحدد عدداً ثابتاً من العناصر لكل صفحة ونستقطع الجزء المطلوب فقط بفضل .slice().
| 1 | startIndex = (currentPage - 1) * itemsPerPage |
| 2 |
| 1 | endIndex = startIndex + itemsPerPage |
| 2 |
| 1 | const itemsPerPage = 5; |
| 2 | const startIndex = (currentPage - 1) * itemsPerPage; |
| 3 | |
| 4 | // استقطاع أجزاء الصفحة الحالية فقط |
| 5 | const paginatedCourses = sortedCourses.slice(startIndex, startIndex + itemsPerPage); |
| 6 | |
| 7 | // حساب إجمالي عدد الصفحات |
| 8 | const totalPages = Math.ceil(sortedCourses.length / itemsPerPage); |
إذا أعيد رسم المكون لأي سبب (مثل فتح القائمة الجانبية أو ضغط زر غير متعلق بالبيانات)، ستُعاد الفلترة والترتيب والتقسيم من جديد بلا أي فائدة! الحل هو تغليف تسلسل العمليات بـ useMemo، لضمان عدم إعادة المعالجة الحسابية إلا إذا تغيرت البيانات الأصلية أو خيارات الفلترة/الترتيب:
| 1 | const processedData = useMemo(() => { |
| 2 | // 1. الفلترة |
| 3 | let result = courses.filter((c) => |
| 4 | c.title.toLowerCase().includes(search.toLowerCase()) |
| 5 | ); |
| 6 | |
| 7 | // 2. الترتيب |
| 8 | result = [...result].sort((a, b) => a.price - b.price); |
| 9 | |
| 10 | // 3. الإرجاع النهائي |
| 11 | return result; |
| 12 | }, [courses, search]); // التحديث يحدث فقط عند تغيير هذه المتغيرات |
⚠️ تحذير
إذا كان المستخدم يصفح في الصفحة رقم 5، ثم قام فجأة بكتابة كلمة بحث أرجعت نتيجتين فقط (أي صفحة واحدة)، فإن معادلة .slice() ستفشل في العثور على أي عناصر في الصفحة رقم 5، وتظهر الواجهة كأنها فارغة تماماً!
مع كل تغيير يطرأ على كائن البحث أو الفلتر، يجب تصفير رقم الصفحة وإرجاع المستخدم فوراً للصفحة رقم 1:
| 1 | const handleSearchChange = (e) => { |
| 2 | setSearchQuery(e.target.value); |
| 3 | setCurrentPage(1); // إعادة تصفير رقم الصفحة فوراً لمنع الواجهات الفارغة |
| 4 | }; |