Інтерактивні таблиці на React: фільтрація, сортування, групування

Робота з великими обсягами даних у B2B-застосунках часто впирається в повільні та незручні таблиці, де фільтрація й сортування перетворюються на очікування. Ми розробляємо інтерактивні таблиці на React з використанням TanStack Table, реалізуючи фільтрацію, сортування та групування під ключ. Наша команда бере на себе весь цикл — від проєктування до впровадження та підтримки, забезпечуючи стабільну роботу навіть із тисячами рядків.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Інтерактивні таблиці на React: фільтрація, сортування, групування
Середній
~3-5 днів
Часті запитання

Ми часто стикаємося із запитами на створення інтерактивних таблиць для B2B-додатків. Основна біль — продуктивність під час фільтрації, сортування та групування тисяч рядків. Розповімо, як ми вирішуємо це завдання за допомогою TanStack Table — headless-бібліотеки, яка дає повний контроль над UI та поведінкою. За 5 років досвіду ми реалізували понад 30 проєктів із таблицями даних, і TanStack Table — наш вибір за замовчуванням. Один із проєктів — CRM-система з таблицею замовлень на 50 000 рядків. Клієнтське сортування призводило до фризів на 10 секунд. Ми запропонували server-side обробку — проблема вирішилася.

Як реалізувати інтерактивні таблиці з фільтрацією, сортуванням та групуванням?

TanStack Table v8 — сучасне рішення для React. Встановлення: npm install @tanstack/react-table. Основні концепції: useReactTable приймає конфігурацію з моделями (getCoreRowModel, getSortedRowModel, getFilteredRowModel, getGroupedRowModel, getExpandedRowModel, getPaginationRowModel). Це дозволяє швидко додати функціональність без boilerplate. Для великих таблиць використовуємо react-virtual для віртуалізації рядків — це знижує навантаження на DOM.

Покрокова інструкція:

  1. Визначте інтерфейс даних, наприклад Order.
  2. Створіть колонки через createColumnHelper.
  3. Налаштуйте стан: sorting, columnFilters, grouping, pagination.
  4. Підключіть моделі рядків.
  5. Рендеріть таблицю: хедери з сортуванням, фільтри для колонок, тіло з групуванням та агрегацією, пагінацію.

Приклад: Таблиця замовлень з групуванням та фільтрацією — інтерактивні таблиці на

import { useReactTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getGroupedRowModel, getExpandedRowModel, getPaginationRowModel, flexRender, createColumnHelper } from '@tanstack/react-table';

interface Order {
  id: string;
  customerName: string;
  status: OrderStatus;
  total: number;
  category: string;
  createdAt: Date;
}

const columnHelper = createColumnHelper<Order>();

const columns = [
  columnHelper.accessor('id', {
    header: '№',
    size: 80,
    cell: ({ getValue }) => <code className="text-xs">{getValue().slice(0, 8)}</code>
  }),
  columnHelper.accessor('customerName', {
    header: 'Покупець',
    filterFn: 'includesString'
  }),
  columnHelper.accessor('status', {
    header: 'Статус',
    cell: ({ getValue }) => <StatusBadge status={getValue()} />,
    filterFn: (row, columnId, filterValue) => filterValue.includes(row.getValue(columnId))
  }),
  columnHelper.accessor('total', {
    header: 'Сума',
    cell: ({ getValue }) => formatCurrency(getValue()),
    sortDescFirst: true,
    aggregationFn: 'sum',
    aggregatedCell: ({ getValue }) => (
      <strong className="font-semibold">{formatCurrency(getValue<number>())}</strong>
    )
  }),
  columnHelper.accessor('category', {
    header: 'Категорія'
  }),
  columnHelper.accessor('createdAt', {
    header: 'Дата',
    cell: ({ getValue }) => format(getValue(), 'dd.MM.yyyy HH:mm'),
    sortingFn: 'datetime'
  })
];

function OrdersTable({ data }) {
  const [sorting, setSorting] = useState([{ id: 'createdAt', desc: true }]);
  const [columnFilters, setColumnFilters] = useState([]);
  const [globalFilter, setGlobalFilter] = useState('');
  const [grouping, setGrouping] = useState<string[]>([]);
  const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 });

  const table = useReactTable({
    data,
    columns,
    state: {
      sorting,
      columnFilters,
      globalFilter,
      grouping,
      pagination
    },
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    onGlobalFilterChange: setGlobalFilter,
    onGroupingChange: setGrouping,
    onPaginationChange: setPagination,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getGroupedRowModel: getGroupedRowModel(),
    getExpandedRowModel: getExpandedRowModel(),
    getPaginationRowModel: getPaginationRowModel()
  });

  return (
    <div>
      {/* Глобальний пошук */}
      <input
        value={globalFilter}
        onChange={e => setGlobalFilter(e.target.value)}
        placeholder="Пошук по всіх полях..."
        className="mb-4 px-3 py-2 border rounded-lg w-64"
      />

      {/* Групування */}
      <div className="flex gap-2 mb-4">
        <span className="text-sm text-gray-600">Групування:</span>
        {['status', 'category'].map(col => (
          <button
            key={col}
            onClick={() => setGrouping(prev =>
              prev.includes(col) ? prev.filter(c => c !== col) : [...prev, col]
            )}
            className={`text-xs px-2 py-1 rounded border ${
              grouping.includes(col) ? 'bg-blue-50 border-blue-300' : 'border-gray-200'
            }`}
          >
            {col}
          </button>
        ))}
      </div>

      {/* Таблиця */}
      <div className="overflow-x-auto rounded-lg border border-gray-200">
        <table className="w-full text-sm">
          <thead className="bg-gray-50">
            {table.getHeaderGroups().map(headerGroup => (
              <tr key={headerGroup.id}>
                {headerGroup.headers.map(header => (
                  <th
                    key={header.id}
                    className="px-4 py-3 text-left font-medium text-gray-600 whitespace-nowrap"
                    style={{ width: header.getSize() }}
                  >
                    <div className="flex items-center gap-1">
                      <span
                        className={header.column.getCanSort() ? 'cursor-pointer select-none' : ''}
                        onClick={header.column.getToggleSortingHandler()}
                      >
                        {flexRender(header.column.columnDef.header, header.getContext())}
                      </span>
                      {header.column.getIsSorted() === 'asc' && ' ↑'}
                      {header.column.getIsSorted() === 'desc' && ' ↓'}
                    </div>
                    {/* Фільтр для кожної колонки */}
                    {header.column.getCanFilter() && (
                      <ColumnFilter column={header.column} />
                    )}
                  </th>
                ))}
              </tr>
            ))}
          </thead>
          <tbody className="divide-y divide-gray-100">
            {table.getRowModel().rows.map(row => (
              <tr key={row.id} className={`hover:bg-gray-50 ${row.getIsGrouped() ? 'bg-gray-50 font-medium' : ''}`}>
                {row.getVisibleCells().map(cell => (
                  <td key={cell.id} className="px-4 py-3">
                    {cell.getIsGrouped() ? (
                      <button onClick={row.getToggleExpandedHandler()} className="flex items-center gap-1">
                        {row.getIsExpanded() ? '▼' : '▶'}
                        {flexRender(cell.column.columnDef.cell, cell.getContext())}
                        ({row.subRows.length})
                      </button>
                    ) : cell.getIsAggregated() ? (
                      flexRender(cell.column.columnDef.aggregatedCell ?? cell.column.columnDef.cell, cell.getContext())
                    ) : flexRender(cell.column.columnDef.cell, cell.getContext())}
                  </td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* Пагінація */}
      <TablePagination table={table} />
    </div>
  );
}

Чому варто обрати server-side обробку для великих таблиць?

При роботі з більш ніж 10 000 рядків клієнтська обробка стає повільною — LCP зростає, користувачі відчувають фризи. Server-side сортування та фільтрація вирішують проблему: дані завантажуються порціями, а сортування виконується на сервері. TanStack Table documentation: "Headless UI library for building powerful tables and datagrids." Приклад реалізації:

function ServerSideTable() {
  const [sorting, setSorting] = useState([]);
  const [columnFilters, setColumnFilters] = useState([]);
  const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 });
  const { data, isLoading } = useQuery({
    queryKey: ['orders', { sorting, columnFilters, pagination }],
    queryFn: () =>
      fetch(
        '/api/orders?' +
          new URLSearchParams({
            sort: sorting.map(s => `${s.id}:${s.desc ? 'desc' : 'asc'}`).join(','),
            filters: JSON.stringify(columnFilters),
            page: String(pagination.pageIndex + 1),
            per_page: String(pagination.pageSize),
          })
      ).then(r => r.json()),
  });
  const table = useReactTable({
    data: data?.items ?? [],
    rowCount: data?.total ?? 0,
    manualSorting: true,
    manualFiltering: true,
    manualPagination: true,
    // ...
  });
}

Порівняння підходів: client-side vs server-side

Критерій Client-side Server-side
Кількість рядків до 10 000 від 10 000
Швидкість сортування миттєво з затримкою мережі
Фільтрація всі рядки запит до API
Групування на клієнті на сервері
Навантаження на браузер високе низьке

Для таблиць з об'ємом даних до 1 000 рядків client-side зазвичай швидше, оскільки не потребує мережевих запитів. При зростанні даних server-side стає єдино можливим варіантом — він у 5-10 разів знижує час відгуку інтерфейсу.

Детальний аналіз продуктивності: при 100 000 рядках client-side рендеринг може займати понад 5 секунд, тоді як server-side з пагінацією по 50 рядків завантажується за 0.2 сек. TanStack Table з virtualized rows (через react-virtual) дозволяє рендерити лише видимі рядки, що ще більше пришвидшує інтерфейс. На відміну від комерційних рішень, TanStack Table — безкоштовна open-source бібліотека, що суттєво економить бюджет проєкту.

Приклад конфігурації агрегації для групування

Для коректного відображення сум у згрупованих рядках потрібно задати aggregationFn для колонки. Наприклад, для колонки total використовуємо aggregationFn: 'sum'. Можна також визначити кастомну функцію.

Які переваги дає групування рядків в інтерактивній таблиці?

Групування дозволяє об'єднувати рядки за значенням колонки, наприклад, за статусом або категорією. Це спрощує аналіз даних: одразу видно кількість записів у кожній групі та агреговані значення (сума, середнє тощо). У TanStack Table групування реалізується через getGroupedRowModel та getExpandedRowModel, а користувач може згортати та розгортати групи. Групування ефективне при роботі з ієрархічними даними та часто використовується в системах аналітики.

Типові помилки при розробці таблиць з фільтрацією та сортуванням

  • Ігнорування віртуалізації: без react-virtual навіть 5 000 рядків можуть викликати затримки. Завжди підключайте @tanstack/react-virtual.
  • Відсутність debounce при фільтрації: кожне введення символу викликає перерисовку. Використовуйте useDebounce із затримкою 300 мс.
  • Неправильне налаштування колонок: size та minSize повинні бути задані, інакше таблиця може роз'їжджатися.
  • Забута агрегація при групуванні: якщо не задати aggregationFn, згруповані рядки покажуть undefined.

Що входить у розробку інтерактивної таблиці під ключ?

  • Аналітика: визначення набору колонок, типів даних, вимог до продуктивності.
  • Проектування: вибір архітектури (client-side/server-side), UI-компонентів.
  • Реалізація: налаштування сортування, фільтрації, групування, пагінації, експорту, кастомних фільтрів (date range, multiselect).
  • Тестування: перевірка на великих об'ємах даних, Edge-кейси, оптимізація Core Web Vitals.
  • Деплой та документація: розміщення на сервері, опис API та інструкція для розробників.
  • Навчання: передача знань команді замовника.

Ми гарантуємо, що таблиця працюватиме з Core Web Vitals у нормі (LCP < 2.5s, CLS < 0.1). Отримайте консультацію щодо вашого проєкту — зв'яжіться з нами.

Типові терміни та етапи

Етап Тривалість
Аналітика 1-2 дні
Проектування та прототип 2-3 дні
Реалізація core-функцій 5-7 днів
Інтеграція з API (server-side) 3-5 днів
Тестування та оптимізація 2-3 дні

Терміни розробки

  • Базова таблиця (сортування, фільтри, пагінація, групування): від 5 до 7 днів.
  • Додавання server-side, експорту в Excel/CSV, кастомних віджетів: ще від 5 до 7 днів.

Вартість розраховується індивідуально, залежить від складності та об'єму даних. Звертайтеся — підберемо оптимальне рішення.

Наші компетенції:

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1394
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1369
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1141
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1060