使用 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> ); }
在這裡,我們接受一系列產品並將它們循環到我們的表中。 它是靜態的,目前不可排序,但現在沒關係。
對數據進行排序
如果你相信所有的白板面試官,你會認為軟件開發幾乎都是排序算法。 幸運的是,我們不會在這裡研究快速排序或冒泡排序。
由於內置的數組函數sort()
,在 JavaScript 中對數據進行排序非常簡單。 它將在沒有額外參數的情況下對數字和字符串數組進行排序:
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> ); }
那麼這裡發生了什麼? 首先,我們創建 products 道具的副本,我們可以隨意更改和更改。 我們需要這樣做,因為Array.prototype.sort
函數會更改原始數組,而不是返回一個新的排序副本。
接下來,我們調用sortedProducts.sort
,並傳遞給它一個sorting
函數。 我們檢查第一個參數a
的name
屬性是否在第二個參數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> ); };
現在,每當我們單擊表格標題時,我們都會更新我們想要排序的字段。 整潔-o!
不過,我們還沒有進行任何實際的排序,所以讓我們來解決這個問題。 還記得之前的排序算法嗎? 就是這樣,只是稍作改動以使用我們的任何字段名稱。
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 中看到該表的演示: