【翻译】使用React创建可排序的表格

696 阅读7分钟

使用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;
  // 创建一个产品产品数组的副本,因为Array.prototype.sort函数将改变原始数组,而不是返回一个新的排序副本
  let sortedProducts = [...products];
  // 传递一个排序函数,检查第一个参数a的name属性是否在第二个参数b之前。
  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>
  );
}


表格排序


我们已经实现了通过name来进行表格的排序,接下来的问题是如果我们需要变更排序的顺序或者使用其他字段来进行排序呢?

为了实现这一功能,我们将使用 useState hook。hook是一种特殊的函数,它允许我们觉察到React的一些核心功能的变化,比如管理状态和触发副作用。这个特定的hook允许我们在组件中保持一个内部状态,如果我们想改变它的话。这是我们要补充的:


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>
    {/* As before */}
    </table>
  );
};


升序 Vs 降序


我们希望看到的下一个特性是在升序和降序之间切换的方法。我们将通过再次单击表标题在升序和降序之间切换。

为了实现这一点,我们需要引入第二种状态——排序顺序。我们将重构当前的sortedField状态变量,以保留字段名及其排序顺序。这个状态变量将不再仅仅只是字符串字符串,而是包含一个带有key(字段名)和direction(排序顺序)的对象。我们会将其重命名为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 });
}


在模板中我们也不要忘了将原先的 setSortedField 行数修改为 requestSort 这一新函数。

到此我们已经基本完成了一个可排序表单应该有的功能,但是这里面还有一个问题。从性能的角度考虑,我们必须确保每一次排序和重新渲染是必要的。建议使用内置的 useMemo hook 来存储所有缓慢的计算吧,以避免当前的实现中每一次渲染都重新排序的问题。


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是一种缓存或记忆复杂计算的方法。当给定相同的输入,如果我们出于某种原因重新渲染组件,则不必对产品进行两次排序。请注意,每当我们的产品发生更改,或者我们根据更改的字段或方向进行排序时,我们都希望触发新的排序。

在这个函数中包装我们的代码将对我们的表排序产生巨大的性能改进!


“组件化”


hooks最棒的一点是让逻辑可重用更加容易。您可能会在整个应用程序中对所有类型的表进行排序,并且必须重新实现相同的内容听起来像是一次次的“复制粘贴”。

React有一个称为custom hooks的特性,可以定制hook以供复用。让我们重构包含在custom hooks的代码,这样我们就可以到处使用它了!


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, 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>
  );
};


原文链接: www.smashingmagazine.com/2020/03/sor…