Создание сортируемых таблиц с помощью 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» , подготовленной дружной командой проекта Интернет-технологии.ру

Меню