إنشاء جداول قابلة للفرز باستخدام React
نشرت: 2022-03-10لطالما كان فرز الجدول مشكلة صعبة للغاية. هناك الكثير من التفاعلات التي يجب تتبعها ، طفرات DOM واسعة النطاق يجب القيام بها وحتى خوارزميات الفرز المعقدة أيضًا. إنها مجرد واحدة من تلك التحديات التي يصعب تصحيحها. حق؟
بدلاً من جذب المكتبات الخارجية ، دعنا نحاول صنع الأشياء بأنفسنا. في هذه المقالة ، سننشئ طريقة قابلة لإعادة الاستخدام لفرز بياناتك المجدولة في React. سنتناول كل خطوة بالتفصيل ، ونتعلم مجموعة من التقنيات المفيدة على طول الطريق.
لن نخوض في بناء جملة React أو JavaScript الأساسي ، لكن ليس عليك أن تكون خبيرًا في React لتتبعها.
إنشاء جدول باستخدام React
أولاً ، لنقم بإنشاء مكون جدول عينة. سيقبل مجموعة من المنتجات ، ويخرج جدولًا أساسيًا للغاية ، يسرد صفًا لكل منتج.
function ProductTable(props) { const { products } = props; return ( <table> <caption>Our products</caption> <thead> <tr> <th>Name</th> <th>Price</th> <th>In Stock</th> </tr> </thead> <tbody> {products.map(product => ( <tr key={product.id}> <td>{product.name}</td> <td>{product.price}</td> <td>{product.stock}</td> </tr> ))} </tbody> </table> ); }
هنا ، نقبل مجموعة من المنتجات ونقوم بإدراجها في طاولتنا. إنه ثابت وغير قابل للفرز في الوقت الحالي ، لكن هذا جيد في الوقت الحالي.
فرز البيانات
إذا كنت تصدق جميع الذين يجرون المقابلات على السبورة البيضاء ، فستعتقد أن تطوير البرامج كان تقريبًا كل خوارزميات الفرز. لحسن الحظ ، لن نبحث في فرز سريع أو نوع فقاعي هنا.
يعد فرز البيانات في JavaScript أمرًا سهلاً للغاية ، وذلك بفضل وظيفة الصفيف المضمنة sort()
. سيقوم بفرز مصفوفات من الأرقام والسلاسل بدون وسيطة إضافية:
const array = ['mozzarella', 'gouda', 'cheddar']; array.sort(); console.log(array); // ['cheddar', 'gouda', 'mozzarella']
إذا كنت تريد شيئًا أكثر ذكاءً قليلاً ، فيمكنك تمريره في وظيفة الفرز. يتم إعطاء هذه الوظيفة عنصرين في القائمة كوسيطات ، وستضع أحدهما أمام الآخر بناءً على ما تقرره.
لنبدأ بفرز البيانات التي نحصل عليها أبجديًا حسب الاسم.
function ProductTable(props) { const { products } = props; let sortedProducts = [...products]; sortedProducts.sort((a, b) => { if (a.name < b.name) { return -1; } if (a.name > b.name) { return 1; } return 0; }); return ( <Table> {/* as before */} </Table> ); }
إذن ما الذي يحدث هنا؟ أولاً ، نقوم بإنشاء نسخة من دعامة المنتجات ، والتي يمكننا تغييرها وتغييرها كما يحلو لنا. نحتاج إلى القيام بذلك لأن الدالة Array.prototype.sort
تغير المصفوفة الأصلية بدلاً من إرجاع نسخة جديدة مرتبة.
بعد ذلك ، نسمي sortedProducts.sort
، ونمرره وظيفة sorting
. نتحقق مما إذا كانت خاصية name
للوسيطة الأولى a
قبل الوسيطة الثانية b
، وإذا كان الأمر كذلك ، فقم بإرجاع قيمة سالبة. يشير هذا إلى أن a
يجب أن يأتي قبل b
في القائمة. إذا كان اسم الوسيطة الأولى بعد اسم الوسيطة الثانية ، فإننا نعيد عددًا موجبًا ، مشيرًا إلى أننا يجب أن نضع b
قبل a
. إذا كان الاثنان متساويين (أي أن كلاهما لهما نفس الاسم) ، فإننا نعيد 0
للحفاظ على الترتيب.
جعل طاولتنا قابلة للفرز
الآن يمكننا التأكد من تصنيف الجدول حسب الاسم - ولكن كيف يمكننا تغيير ترتيب الفرز بأنفسنا؟
لتغيير الحقل الذي نقوم بالفرز وفقًا له ، نحتاج إلى تذكر الحقل المصنف حاليًا. سنفعل ذلك باستخدام خطاف useState
.
الخطاف هو نوع خاص من الوظائف التي تتيح لنا "ربط" بعض وظائف React الأساسية ، مثل إدارة الحالة وإثارة الآثار الجانبية. يتيح لنا هذا الخطاف المعين الحفاظ على جزء من الحالة الداخلية في مكوننا ، وتغييره إذا أردنا ذلك. هذا ما سنضيفه:
const [sortedField, setSortedField] = React.useState(null);
نبدأ بعدم فرز أي شيء على الإطلاق. بعد ذلك ، دعنا نغير عناوين الجدول لتضمين طريقة لتغيير الحقل الذي نريد الفرز وفقًا له.
const ProductsTable = (props) => { const { products } = props; const [sortedField, setSortedField] = React.useState(null); return ( <table> <thead> <tr> <th> <button type="button" onClick={() => setSortedField('name')}> Name </button> </th> <th> <button type="button" onClick={() => setSortedField('price')}> Price </button> </th> <th> <button type="button" onClick={() => setSortedField('stock')}> In Stock </button> </th> </tr> </thead> {/* As before */} </table> ); };
الآن ، عندما ننقر فوق عنوان جدول ، نقوم بتحديث الحقل الذي نريد الفرز وفقًا له. أنيق يا!
نحن لا نقوم بأي فرز فعلي حتى الآن ، لذلك دعونا نصلح ذلك. تذكر خوارزمية الفرز من قبل؟ هنا ، تم تغييره قليلاً للعمل مع أي من أسماء الحقول الخاصة بنا.
const ProductsTable = (props) => { const { products } = props; const [sortedField, setSortedField] = React.useState(null); let sortedProducts = [...products]; if (sortedField !== null) { sortedProducts.sort((a, b) => { if (a[sortedField] < b[sortedField]) { return -1; } if (a[sortedField] > b[sortedField]) { return 1; } return 0; }); } return ( <table>
نتأكد أولاً من أننا اخترنا حقلاً للفرز وفقًا له ، وإذا كان الأمر كذلك ، فنحن نفرز المنتجات حسب هذا الحقل.
تصاعدي مقابل تنازلي
الميزة التالية التي نريد رؤيتها هي وسيلة للتبديل بين الترتيب التصاعدي والتنازلي. سنقوم بالتبديل بين الترتيب التصاعدي والتنازلي بالنقر فوق عنوان الجدول مرة أخرى.

لتنفيذ ذلك ، سنحتاج إلى تقديم جزء آخر من الحالة - ترتيب الفرز. سنقوم بإعادة تشكيل متغير حالة sortedField
الحالي للحفاظ على اسم الحقل واتجاهه. بدلاً من احتواء سلسلة ، سيحتوي متغير الحالة هذا على كائن بمفتاح (اسم الحقل) واتجاه. سنعيد تسميته إلى sortConfig
ليكون أكثر وضوحًا.
ها هي وظيفة الفرز الجديدة:
sortedProducts.sort((a, b) => { if (a[sortConfig.key] < b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? -1 : 1; } if (a[sortConfig.key] > b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? 1 : -1; } return 0; });
الآن ، إذا كان الاتجاه "تصاعديًا" ، سنفعل كما فعلنا سابقًا. إذا لم يكن الأمر كذلك ، فسنقوم بالعكس ، مما يعطينا ترتيبًا تنازليًا.
بعد ذلك ، سننشئ وظيفة جديدة - requestSort
- والتي ستقبل اسم الحقل ، وتقوم بتحديث الحالة وفقًا لذلك.
const requestSort = key => { let direction = 'ascending'; if (sortConfig.key === key && sortConfig.direction === 'ascending') { direction = 'descending'; } setSortConfig({ key, direction }); }
سيتعين علينا أيضًا تغيير معالجات النقرات الخاصة بنا لاستخدام هذه الوظيفة الجديدة!
return ( <table> <thead> <tr> <th> <button type="button" onClick={() => requestSort('name')}> Name </button> </th> <th> <button type="button" onClick={() => requestSort('price')}> Price </button> </th> <th> <button type="button" onClick={() => requestSort('stock')}> In Stock </button> </th> </tr> </thead> {/* as before */} </table> );
بدأنا الآن في الظهور بمظهر مكتمل للميزات ، ولكن لا يزال هناك شيء واحد كبير يتعين القيام به. نحتاج إلى التأكد من أننا نفرز بياناتنا فقط عندما نحتاج إلى ذلك. في الوقت الحالي ، نقوم بفرز جميع بياناتنا في كل عملية تصيير ، مما سيؤدي إلى جميع أنواع مشكلات الأداء في المستقبل. بدلاً من ذلك ، دعنا نستخدم خطاف useMemo
المدمج لحفظ جميع الأجزاء البطيئة في الذاكرة!
const ProductsTable = (props) => { const { products } = props; const [sortConfig, setSortConfig] = React.useState(null); React.useMemo(() => { let sortedProducts = [...products]; if (sortedField !== null) { sortedProducts.sort((a, b) => { if (a[sortConfig.key] < b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? -1 : 1; } if (a[sortConfig.key] > b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? 1 : -1; } return 0; }); } return sortedProducts; }, [products, sortConfig]);
إذا لم تكن قد رأيتها من قبل ، useMemo
هي طريقة لتخزين - أو حفظ - عمليات حسابية باهظة الثمن. إذن ، بالنظر إلى نفس المدخلات ، ليس من الضروري فرز الضربات مرتين إذا أعدنا تصيير المكون لسبب ما. لاحظ أننا نريد بدء فرز جديد كلما تغيرت منتجاتنا ، أو المجال أو الاتجاه الذي نقوم بالفرز حسب التغييرات.
إن تغليف الكود الخاص بنا في هذه الوظيفة سيكون له آثار كبيرة على الأداء لفرز جدولنا!
مما يجعلها قابلة لإعادة الاستخدام
أحد أفضل الأشياء في الخطافات هو مدى سهولة جعل المنطق قابلاً لإعادة الاستخدام. من المحتمل أن تقوم بفرز جميع أنواع الجداول في جميع أنحاء التطبيق الخاص بك ، ويبدو أن الاضطرار إلى إعادة تطبيق نفس الأشياء مرة أخرى يشبه السحب.
يحتوي React على هذه الميزة التي تسمى الخطافات المخصصة. إنها تبدو رائعة ، لكنها كلها وظائف عادية تستخدم خطافات أخرى بداخلها. دعونا نعيد تشكيل الكود الخاص بنا ليتم تضمينه في خطاف مخصص ، حتى نتمكن من استخدامه في كل مكان!
const useSortableData = (items, config = null) => { const [sortConfig, setSortConfig] = React.useState(config); const sortedItems = React.useMemo(() => { let sortableItems = [...items]; if (sortConfig !== null) { sortableItems.sort((a, b) => { if (a[sortConfig.key] < b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? -1 : 1; } if (a[sortConfig.key] > b[sortConfig.key]) { return sortConfig.direction === 'ascending' ? 1 : -1; } return 0; }); } return sortableItems; }, [items, sortConfig]); const requestSort = key => { let direction = 'ascending'; if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') { direction = 'descending'; } setSortConfig({ key, direction }); } return { items: sortedItems, requestSort }; }
هذا إلى حد كبير نسخ ولصق من الكود السابق ، مع القليل من إعادة التسمية. يقبل useSortableData
العناصر ، وحالة فرز أولية اختيارية. تقوم بإرجاع كائن بالعناصر التي تم فرزها ، ووظيفة لإعادة فرز العناصر.
يبدو رمز الجدول الخاص بنا الآن كما يلي:
const ProductsTable = (props) => { const { products } = props; const { items, requestSort } = useSortableData(products); return ( <table>{/* ... */}</table> ); };
لمسة أخيرة
هناك قطعة صغيرة مفقودة - طريقة للإشارة إلى كيفية ترتيب الجدول. للإشارة إلى أنه في تصميمنا ، نحتاج إلى إعادة الحالة الداخلية أيضًا - sortConfig
. دعنا نعيد ذلك أيضًا ، ونستخدمه لإنشاء أنماط يمكننا تطبيقها على عناوين الجداول الخاصة بنا!
const ProductTable = (props) => { const { items, requestSort, sortConfig } = useSortableData(props.products); const getClassNamesFor = (name) => { if (!sortConfig) { return; } return sortConfig.key === name ? sortConfig.direction : undefined; }; return ( <table> <caption>Products</caption> <thead> <tr> <th> <button type="button" onClick={() => requestSort('name')} className={getClassNamesFor('name')} > Name </button> </th> {/* … */} </tr> </thead> {/* … */} </table> ); };
وبهذا ، انتهينا!
تغليف
كما اتضح ، لم يكن إنشاء خوارزمية فرز الجدول الخاصة بك عملاً مستحيلاً بعد كل شيء. لقد وجدنا طريقة لنمذجة حالتنا ، وكتبنا وظيفة فرز عامة ، وكتبنا طريقة لتحديث تفضيلات الفرز لدينا. لقد تأكدنا من أن كل شيء كان ذا أداء جيد وأعدنا بنائه جميعًا في خطاف مخصص. أخيرًا ، قدمنا طريقة للإشارة إلى ترتيب الفرز للمستخدم.
يمكنك مشاهدة عرض توضيحي للجدول في CodeSandbox هذا: