Создание сортируемых таблиц с помощью React

В этом руководстве с помощью 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']

Если нужна более сложная сортировка, используйте функцию sorting. Она получает два элемента списка в качестве аргументов и размещает их один перед другим.

Начнем с сортировки данных по имени в алфавитном порядке.

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>
      {/* как и раньше */}
    </Table>
  );
}

Сначала мы создаем копию товара, которую можем изменять по своему усмотрению с помощью Array.prototype.sort. Данный метод изменяет исходный массив, а не возвращает новую отсортированную копию.

Далее мы вызываем sortedProducts.sort и передаем ей функцию sorting. Затем проверяем, располагается ли свойство name первого аргумента a раньше свойства name второго аргумента b. Если да, то возвращаем отрицательное значение.

Это указывает на то, что a должно предшествовать в списке b. Если name первого аргумента находится после name второго, возвращаем положительное число. Оно указывает на то, что нужно поместить b перед a. Если они равны (оба имеют одинаковое name), возвращаем 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.

Пожалуйста, опубликуйте свои комментарии по текущей теме статьи. За комментарии, лайки, дизлайки, отклики, подписки огромное вам спасибо!

Данная публикация является переводом статьи «Creating Sortable Tables With React» , подготовленная редакцией проекта.

Меню
Posting....