使用 React 创建可排序表

已发表: 2022-03-10
快速总结 ↬在 React 中使您的表可排序听起来可能是一项艰巨的任务,但它不必太难。 在本文中,我们将实现所有您需要的所有表格排序需求。

表格排序一直是一个很难解决的问题。 有很多交互需要跟踪,大量的 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函数。 我们检查第一个参数aname属性是否在第二个参数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 中看到该表的演示: