Создание сортируемых таблиц с помощью 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> ); }
Здесь мы принимаем массив продуктов и зацикливаем их в нашей таблице. На данный момент он статичен и не поддается сортировке, но пока это нормально.
Сортировка данных
Если бы вы поверили всем интервьюерам с доски, вы бы подумали, что разработка программного обеспечения — это почти все алгоритмы сортировки. К счастью, мы не будем рассматривать здесь быструю или пузырьковую сортировку.
Сортировка данных в JavaScript довольно проста благодаря встроенной функции обработки массивов sort()
. Он будет сортировать массивы чисел и строк без дополнительного аргумента:
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> ); }
Так что же здесь происходит? Во-первых, мы создаем копию свойства продуктов, которую мы можем изменять и изменять по своему усмотрению. Нам нужно сделать это, потому что функция Array.prototype.sort
изменяет исходный массив вместо того, чтобы возвращать новую отсортированную копию.
Затем мы вызываем sortedProducts.sort
и передаем ему функцию sorting
. Мы проверяем, находится ли свойство name
первого аргумента a
перед вторым аргументом 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> ); };
Теперь всякий раз, когда мы щелкаем заголовок таблицы, мы обновляем поле, по которому хотим выполнить сортировку. Аккуратно-о!
Однако мы еще не занимаемся сортировкой, так что давайте это исправим. Помните алгоритм сортировки из предыдущего? Вот он, только слегка измененный для работы с любым из наших имен полей.
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: