«Как создать сортируемую и фильтруемую таблицу в React – CodesCode»
Изучите, как создать компонент таблицы в React, позволяющий сортировать и фильтровать данные, что поможет ускорить процессы при работе с большими наборами данных.
Динамические таблицы часто используются в веб-приложениях для представления данных в структурированном формате. Сортировка и фильтрация набора данных могут ускорить процессы при работе с большими наборами данных. В этом руководстве мы рассмотрим, как создать сортируемую и фильтруемую компоненту таблицы в React.
Вы можете найти полный исходный код в одном куске, размещенном на GitHub. Конечный результат изображен ниже.
- Предварительные требования
- Настройка проекта
- Создание компонента
- Стилизация таблицы
- Добавление элементов управления
- Фильтрация таблицы
- Сортировка таблицы
- Обработка избыточной фильтрации
- Заключение
Предварительные требования
Перед началом работы с этим руководством предполагается, что у вас есть базовые знания HTML, CSS, JavaScript и React. Хотя мы пошагово рассмотрим проект, мы не будем подробно объяснять основные концепции в React или методы массивов JavaScript. Мы также будем использовать TypeScript, но то же самое можно достичь и без него. С этим сказано, давайте приступим к кодированию.
Настройка проекта
Для этого проекта мы будем использовать Vite, мощный и популярный фронтенд-инструмент. Если у вас еще нет существующего приложения React, вы можете создать новый проект в Vite, используя одну из следующих команд в терминале:
# Using NPMnpm create vite@latest folder-name -- --template react-ts# Using Yarnyarn create vite folder-name --template react-ts# Using PNPMpnpm create vite folder-name --template react-ts# Using Bunbunx create-vite folder-name --template react-ts
Когда вы будете готовы, создайте новую папку для компонента Table
в проекте React со следующей структурой:
src├─ components│ ├─ Table│ │ ├─ index.ts │ │ ├─ table.css│ │ ├─ Table.tsx├─ App.tsx
index.ts
. В этом файле мы будем экспортироватьTable.tsx
, чтобы упростить пути импорта.table.css
. Содержит стили, связанные с компонентом. В этом учебнике мы будем использовать чистый CSS.Table.tsx
. Сам компонент.
Откройте файл Table.tsx
и экспортируйте следующее, чтобы мы могли проверить, загружается ли компонент при его импорте:
import './table.css'export const Table = () => { return ( <h1>Table component</h1> )}
Внутри файла index.ts
переэкспортируйте компонент, используя следующую строку:
export * from './Table'
Теперь, когда у нас есть настроенные компоненты файлы, давайте проверим, загружается ли он, импортировав его в наше приложение. В этом учебнике мы будем использовать компонент App
. Если у вас есть существующий проект React, вы можете импортировать его в нужное место. Импортируйте компонент Table
в свое приложение так:
import { Table } from './components/Table'const App = () => { return ( <Table /> )}export default App
Генерация псевдо-данных
Конечно, для работы с таблицей нам понадобятся сначала некоторые псевдо-данные. Для этого урока мы можем использовать Генератор JSON, бесплатный сервис для генерации случайных данных JSON. Мы будем использовать следующую схему для генерации данных:
[ '{{repeat(10)}}', { id: '{{index()}}', name: '{{firstName()}} {{surname()}}', company: '{{company().toUpperCase()}}', active: '{{bool()}}', country: '{{country()}}' }]
JSON Генератор поставляется с различными встроенными функциональностями для генерации различных типов данных. Вышеприведенная схема создаст массив объектов с десятью случайными объектами в форме:
{ id: 0, // number - Index of the array, starting from 0 name: 'Jaime Wallace', // string - A random name company: 'UBERLUX', // string - Capitalized random string active: false, // boolean - either `true` or `false` country: 'Peru' // string - A random country name}
Сгенерируйте список записей, используя схему выше, затем создайте новый файл внутри папки src
с названием data.ts и экспортируйте массив следующим образом:
export const data = [ { id: 0, name: 'Jaime Wallace', company: 'UBERLUX', active: false, country: 'Peru' }, { ... },]
Откройте файл App.tsx
, и передайте эти данные в компонент Table
в виде свойства с именем rows
. Мы сгенерируем таблицу на основе этих данных:
import { Table } from './components/Table'+ import { data } from './data' const App = () => { return (- <Table />+ <Table rows={data} /> ) } export default App
Создание компонента
Теперь, когда у нас есть и компонент, и настроенный набор данных, мы можем приступить к работе с таблицей. Чтобы динамически генерировать таблицу на основе переданных данных, замените все в компоненте Table
следующими строками кода:
import { useState } from 'react'import './table.css'export const Table = ({ rows }) => { const [sortedRows, setRows] = useState(rows) return ( <table> <thead> <tr> {Object.keys(rows[0]).map((entry, index) => ( <th key={index}>{entry}</th> ))} </tr> </thead> <tbody> {sortedRows.map((row, index) => ( <tr key={index}> {Object.values(row).map((entry, columnIndex) => ( <td key={columnIndex}>{entry}</td> ))} </tr> ))} </tbody> </table> )}
Это будет динамически генерировать заголовки и ячейки таблицы на основе свойства rows
. Разберем, как это работает. Поскольку мы собираемся сортировать и фильтровать строки, нам нужно сохранить их в состоянии с помощью хука useState
. Свойство передается в качестве начального значения хуку.
Чтобы отобразить заголовки таблицы, мы можем использовать Object.keys
для первой записи в массиве, что вернет ключи объекта в виде списка строк:
const rows = [ { id: 0, name: 'Jaime Wallace' }, { ... }]// #1 Turn object properties into an array of keys:Object.keys(rows[0]) -> ['id', 'name']// #2 Chain `map` from the array to display the values inside `th` elements:['id', 'name'].map((entry, index) => (...))
Чтобы отобразить ячейки таблицы, нам нужно использовать Object.values
на каждой строке, которая возвращает значение каждого ключа в объекте, в отличие от Object.keys
. Подробно, вот как мы отображаем ячейки таблицы:
const sortedRows = [ { id: 0, name: 'Jaime Wallace' }, { ... }]// #1 Loop through each object in the array and create a `tr` element:{sortedRows.map((row, index) => (<tr key={index}>...</tr>))}// #2 Loop through each property of each object to create the `td` elements:Object.values(row) -> [0, 'Jaime Wallace']
Этот подход делает использование любого типа данных с нашим компонентом Table
ä очень гибким, без необходимости переписывать логику. На данный момент, у нас есть следующая таблица, созданная с использованием нашего компонента. Однако, у таблицы есть некоторые проблемы с форматированием.
Форматирование ячеек таблицы
На данный момент, колонка active
не отображается. Это происходит потому, что значения для этих полей являются логическими и не выводятся как строки в JSX. Для решения этой проблемы, мы можем добавить новую функцию для форматирования записей на основе их значений. Добавьте следующий код в компонент Table
и оберните entry
в эту функцию в JSX:
const formatEntry = (entry: string | number | boolean) => { if (typeof entry === 'boolean') { return entry ? '✅' : '❌' } return entry}return ( <table> <thead>...</thead> <tbody> {sortedRows.map((row, index) => ( <tr key={index}> {Object.values(row).map((entry, columnIndex) => ( <td key={columnIndex}>{formatEntry(entry)}</td> ))} </tr> ))} </tbody> </table>)
Функция formatEntry
ожидает вход, который в нашем случае может быть либо строкой
, либо числом
, либо логическим значением
, а затем возвращает отформатированное значение, если typeof entry
является логическим значением
. Это означает, что для значений true
мы отобразим зеленую галочку, а для значений false
мы отобразим красный крест. Используя похожий подход, мы также можем форматировать заголовки таблицы. Давайте сделаем их с заглавной буквы с помощью следующей функции:
export const capitalize = ( str: string) => str?.replace(/\b\w/g, substr => substr.toUpperCase())
Эта функция использует регулярное выражение для получения первой буквы каждого слова и преобразования ее в верхний регистр. Чтобы использовать эту функцию, мы можем создать файл utils.ts
в корне папки src
, экспортировать эту функцию, а затем импортировать ее в наш компонент Table
для использования следующим образом:
import { capitalize } from '../../utils'export const Table = ({ rows }) => { ... return ( <table> <thead> <tr> {Object.keys(rows[0]).map((entry, index) => ( <th key={index}>{capitalize(entry)}</th> ))} </tr> </thead> <tbody>...</tbody> </table> )}
Основываясь на этих изменениях, мы теперь имеем динамически созданную, отформатированную таблицу.
Набор типов props
Прежде чем мы приступим к стилизации таблицы, а затем добавлению элементов управления, давайте правильно указывать тип пропса rows
. Для этого мы можем создать файл types.ts
в корне папки src
и экспортировать пользовательские типы, которые могут быть использованы повторно в проекте. Создайте файл и экспортируйте следующий тип:
export type Data = { id: number name: string company: string active: boolean country: string}[]
Для ввода свойства rows
в компоненте Table
просто импортируйте этот тип и передайте его компоненту следующим образом:
import { Data } from '../../types'export type TableProps = { rows: Data}export const Table = ({ rows }: TableProps) => { ... }
Оформление таблицы
Для оформления всего компонента таблицы нам потребуется всего несколько правил. Во-первых, мы хотим установить цвета и границы, для чего мы можем использовать следующие стили:
table { width: 100%; border-collapse: collapse;}thead { text-align: left; /* `thead` is centered by default */ color: #939393; background: #2f2f2f;}th,td { padding: 4px 6px; border: 1px solid #505050;}
Добавьте вышеприведенный код в table.css
. Убедитесь, что установили border-collapse
в collapse
для <table>
, чтобы избежать двойных границ. Поскольку таблица занимает весь экран, давайте также внесем некоторые изменения и удалим левую и правую границу, так как они все равно не видны:
th:first-child,td:first-child { border-left: 0;}th:last-child,th:last-child { border-right: 0;}
Это позволит избавиться от границ по краям элемента <table>
, что приведет к более чистому внешнему виду. Наконец, давайте добавим эффект наведения на строки таблицы, чтобы визуально помочь пользователям при поиске в таблице:
tr:hover { background: #2f2f2f;}
После всех проделанных действий, у нас теперь есть следующее поведение для компонента.
Добавление элементов управления
Теперь, когда мы стилизовали таблицу, давайте добавим элементы управления для функциональности сортировки и фильтрации. Мы создадим <input>
для фильтра и элемент <select>
для сортировки. Также добавим кнопку для переключения между порядком сортировки (возрастание/убывание).
Для добавления полей ввода, нам также потребуются новые состояния для текущего порядка (возрастание или убывание) и переменная, чтобы отслеживать ключ сортировки (какой ключ в объекте используется для сортировки). Исходя из этого, расширьте компонент Table
следующим образом:
const [order, setOrder] = useState('asc')const [sortKey, setSortKey] = useState(Object.keys(rows[0])[0])const filter = (event: React.ChangeEvent<HTMLInputElement>) => {}const sort = (value: keyof Data[0], order: string) => {}const updateOrder = () => {}return ( <> <div className="controls"> <input type="text" placeholder="Filter items" onChange={filter} /> <select onChange={(event) => sort()}> {Object.keys(rows[0]).map((entry, index) => ( <option value={entry} key={index}> Order by {capitalize(entry)} </option> ))} </select> <button onClick={updateOrder}>Switch order ({order})</button> </div> <table>...</table> </>)
Давайте двигаться по порядку, чтобы понять, что изменилось:
order
. Сначала нам нужно создать новое состояние для порядка сортировки. Это может бытьasc
илиdesc
. Мы будем использовать его значение в функцииsort
.sortKey
. Нам также нужно состояние для ключа сортировки. По умолчанию мы можем получить ключ первого свойства в нашем массиве объектов, используяObject.keys(rows[0])[0]
. Мы будем использовать его, чтобы отслеживать сортировку при переключении между порядками.filter
. Нам понадобится функция для фильтрации результатов. Ее нужно передать в событиеonChange
элемента<input>
. Обратите внимание, чтоReact.ChangeEvent
является обобщенным и может принимать тип HTML элемента, вызвавшего изменение.sort
. Как и функцияfilter
, ее нужно присоединить к событиюonChange
, но на этот раз – к элементу<select>
. Она будет принимать два параметра:
value
. Она может принимать ключи нашего объекта данных. Мы можем указать тип с помощью ключевого словаkeyof
. Это означает, чтоvalue
может быть одним из ключейid
,name
,company
,active
илиcountry
.order
. Порядок сортировки, либоasc
, либоdesc
.
updateOrder
. Наконец, нам также нужна функция для обновления порядка. Она будет вызываться при нажатии кнопки.Обратите внимание, что мы используем ту же логику, что и для элементов <th>
, для динамического генерирования вариантов в <select>
. Мы также можем использовать утилитарную функцию capitalize
для форматирования вариантов.
Стилизация контролов
Давайте стилизуем контролы, прежде чем продолжить. Это можно сделать всего лишь с помощью нескольких CSS-правил. Расширьте файл table.css
следующим образом:
.controls { display: flex;}input,select { flex: 1; padding: 5px 10px; border: 0;}button { background: #2f2f2f; color: #FFF; border: 0; cursor: pointer; padding: 5px 10px;}
Это обеспечит выравнивание вводимых значений рядом друг с другом. Используя flex: 1
для элементов <input>
и <select>
, мы можем сделать так, чтобы они занимали равное количество ширины из доступного пространства. Кнопка <button>
будет занимать столько места, сколько необходимо для текста, который на ней находится.
Фильтрация таблицы
Теперь, когда у нас есть элементы управления, давайте посмотрим на реализацию функционала. Для фильтрации таблицы по любому полю, нам необходимо придерживаться следующей логики:
const rows = [ { id: 0, name: 'Jaime Wallace' }, { ... }]// #1: Set `rows` to a filtered version using `filter`// The return value of `filter` will determine which rows to keepsetRows([ ...rows ].filter(row => { ... }))// From here on, we discuss the return value of `filter`// #2: Grab every field from the `row` object to use it for filteringObject.values(row) -> [0, 'Jaime Wallace']// #3: Join the values together into a single string[0, 'Jaime Wallace'].join('') -> '0Jaime Wallace'// #4: Convert the string into lowercase to make search case-insensitive'0Jaime Wallace'.toLowerCase() -> '0jaime wallace'// #5: Check if the string contains the value entered in the input'0jaime wallace'.includes(value) -> true / false
С учетом всех этих моментов, мы можем создать значение return
для функции filter
на основе вышеуказанной логики. Это дает нам следующую реализацию для функции filter
:
const filter = (event: React.ChangeEvent<HTMLInputElement>) => { const value = event.target.value if (value) { setRows([ ...rows.filter(row => { return Object.values(row) .join('') .toLowerCase() .includes(value) }) ]) } else { setRows(rows) }}
Обратите внимание, что мы также хотим проверить, присутствует ли значение value
. Его отсутствие означает, что поле <input>
пустое. В таких случаях мы хотим сбросить состояние и передать нефильтрованные rows
в setRows
, чтобы сбросить таблицу.
Сортировка таблицы
У нас есть функциональность фильтрации, но нам все еще не хватает сортировки. Для сортировки у нас есть две отдельные функции:
sort
. Функция, которая будет обрабатывать сортировку.updateOder
. Функция, которая будет переключать порядок сортировки с возрастания на убывание и наоборот.
Давайте начнем с функции сортировки. При каждом изменении элемента <select>
будет вызываться функция sort
. Мы хотим использовать значение элемента <select>
для определения ключа, который будет использоваться для сортировки. Для этого мы можем использовать простой метод sort
и квадратные скобки для динамического сравнения ключей объекта:
const sort = (value: keyof Data[0], order: string) => { const returnValue = order === 'desc' ? 1 : -1 setSortKey(value) setRows([ ...sortedRows.sort((a, b) => { return a[value] > b[value] ? returnValue * -1 : returnValue }) ])}
Давайте пройдемся по функции сверху вниз для лучшего понимания реализации.
returnValue
. Основываясь на состоянииorder
, мы хотим, чтобы возвращаемое значение было либо 1, либо -1. Это помогает нам определить порядок сортировки (1 для убывающего и -1 для возрастающего).setSortKey
. Значение, переданное в функцию, является значением элемента<select>
. Мы хотим записать это значение в наше состояние (sortKey
), что мы можем сделать, вызывая функцию обновленияsetSortKey
.setRows
. Фактическая сортировка происходит в этом вызове. Используя квадратные скобки, мы можем сравнитьa[value]
сb[value]
и вернуть либо -1, либо 1.
Давайте возьмем следующий пример:
const rows = [{ id: 0 }, { id: 1 }]const value = 'id'// This translate to a.id and b.idrows.sort((a, b) => a[value] > b[value] ? -1 : 1)// If `returnValue` is -1, the order changesrows.sort((a, b) => a[value] > b[value] ? 1 : -1)
Переключение между порядками сортировки
Для обновления порядка сортировки достаточно обновить состояние order
при каждом щелчке по кнопке. Мы можем достичь этого с помощью следующей функциональности:
const updateOrder = () => { const updatedOrder = order === 'asc' ? 'desc' : 'asc' setOrder(updatedOrder) sort(sortKey as keyof Data[0], updatedOrder)}
Это установит порядок в противоположное значение при каждом нажатии. Обратите внимание, что после обновления состояния order
с помощью setOrder
, нам также нужно вызвать функцию sort
, чтобы сортировать таблицу на основе обновленного порядка. Чтобы вывести правильный тип для переменной sortKey
, мы можем ссылаться на ключи типа Data
, используя приведение типа: as keyof Data[0]
. В качестве второго параметра мы также должны передать обновленный порядок.
Обработка избыточной фильтрации
Чтобы завершить этот проект, давайте добавим некоторую индикацию для состояния избыточной фильтрации. Мы хотим показать состояние избыточной фильтрации только при отсутствии результатов. Это легко сделать, проверив длину нашего состояния sortedRows
. После элемента <table>
добавьте следующее:
return ( <> <div className="controls">...</div> <table>...</table> {!sortedRows.length && ( <h1>No results... Try expanding the search</h1> )} </>)
Вывод
В заключение, построение сортируемой и фильтруемой таблицы в React не обязательно сложное. С помощью методов массивов и цепочки функций, используя правильную функциональность, мы можем создавать лаконичные и точные функции для обработки этих задач. Все включено, мы смогли поместить всю логику в менее чем 100 строк кода.
Как видно в начале этого руководства, весь проект доступен целиком на GitHub. Спасибо за чтение; приятного кодинга!
Leave a Reply