Реалізація інтерактивних таблиць (DataTables/TanStack Table) на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

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

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

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

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація інтерактивних таблиць (DataTables/TanStack Table) на сайті
Середня
~3-5 робочих днів
Часті питання

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

Етапи розробки

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

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація інтерактивних таблиць (DataTables/TanStack Table) на сайті

Таблиця зі сортуванням та пагінацією — одна з найчастіших задач у фронтенді, та одна з найчастіше реалізуємих погано. jQuery DataTables досі зустрічається в legacy-проектах, але в сучасних React-додатках стандартом де-факто став TanStack Table (раніше react-table) — headless-бібліотека без вбудованих стилів, яка дає повний контроль над розміткою.

Вибір бібліотеки

TanStack Table v8 — для React/Vue/Solid/Svelte проектів. Headless: жодних стилів в комплекті, лише логіка. Розмір ~14 КБ gzipped.

jQuery DataTables — виправдано лише якщо сайт вже використовує jQuery та немає сенсу додавати Reactради однієї таблиці. В інших випадках — не варто.

AG Grid Community — коли потрібна віртуалізація на 100 000+ рядків, Excel-експорт та редагування ячейок. Важче, але потужніше.

TanStack Table: базова реалізація

npm install @tanstack/react-table

Визначаємо колонки та підключаємо хук:

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

type Order = {
  id: string
  customer: string
  amount: number
  status: 'pending' | 'paid' | 'cancelled'
  createdAt: string
}

const columnHelper = createColumnHelper<Order>()

const columns = [
  columnHelper.accessor('id', {
    header: '№ замовлення',
    cell: (info) => <span className="font-mono text-sm">{info.getValue()}</span>,
  }),
  columnHelper.accessor('customer', {
    header: 'Клієнт',
    enableSorting: true,
  }),
  columnHelper.accessor('amount', {
    header: 'Сума',
    cell: (info) => `${info.getValue().toLocaleString('uk-UA')} ₴`,
    sortingFn: 'basic',
  }),
  columnHelper.accessor('status', {
    header: 'Статус',
    cell: (info) => <StatusBadge status={info.getValue()} />,
    enableSorting: false,
  }),
  columnHelper.accessor('createdAt', {
    header: 'Дата',
    sortingFn: 'datetime',
  }),
]
function OrdersTable({ data }: { data: Order[] }) {
  const [sorting, setSorting] = useState<SortingState>([])
  const [globalFilter, setGlobalFilter] = useState('')

  const table = useReactTable({
    data,
    columns,
    state: { sorting, globalFilter },
    onSortingChange: setSorting,
    onGlobalFilterChange: setGlobalFilter,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    initialState: { pagination: { pageSize: 25 } },
  })

  return (
    <div>
      <input
        value={globalFilter}
        onChange={(e) => setGlobalFilter(e.target.value)}
        placeholder="Пошук по всім полям..."
        className="mb-4 w-64 border rounded px-3 py-2"
      />
      <table className="w-full border-collapse">
        <thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th
                  key={header.id}
                  onClick={header.column.getToggleSortingHandler()}
                  className={header.column.getCanSort() ? 'cursor-pointer select-none' : ''}
                >
                  {flexRender(header.column.columnDef.header, header.getContext())}
                  {{ asc: ' ↑', desc: ' ↓' }[header.column.getIsSorted() as string] ?? ''}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map((row) => (
            <tr key={row.id} className="hover:bg-gray-50">
              {row.getVisibleCells().map((cell) => (
                <td key={cell.id}>
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
      <div className="flex items-center gap-2 mt-4">
        <button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
          ←
        </button>
        <span>
          Сторінка {table.getState().pagination.pageIndex + 1} з {table.getPageCount()}
        </span>
        <button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
          →
        </button>
        <select
          value={table.getState().pagination.pageSize}
          onChange={(e) => table.setPageSize(Number(e.target.value))}
        >
          {[10, 25, 50, 100].map((size) => (
            <option key={size} value={size}>по {size}</option>
          ))}
        </select>
      </div>
    </div>
  )
}

Серверна пагінація

Для великих датасетів (10 000+ рядків) пагінація на клієнті не працює — все має йти через API:

const [{ pageIndex, pageSize }, setPagination] = useState({
  pageIndex: 0,
  pageSize: 25,
})

// React Query для завантаження даних
const { data, isFetching } = useQuery({
  queryKey: ['orders', pageIndex, pageSize, sorting, globalFilter],
  queryFn: () =>
    fetchOrders({
      page: pageIndex + 1,
      limit: pageSize,
      sortBy: sorting[0]?.id,
      sortDir: sorting[0]?.desc ? 'desc' : 'asc',
      search: globalFilter,
    }),
  keepPreviousData: true, // не миготить при переході між сторінками
})

const table = useReactTable({
  data: data?.rows ?? [],
  columns,
  pageCount: data?.pageCount ?? -1,
  state: { sorting, pagination: { pageIndex, pageSize }, globalFilter },
  manualPagination: true,  // ключовий флаг
  manualSorting: true,
  manualFiltering: true,
  onPaginationChange: setPagination,
  // ...
})

Експорт у CSV

function exportToCSV(table: Table<Order>) {
  const headers = table.getAllColumns()
    .filter((col) => col.getIsVisible())
    .map((col) => col.columnDef.header as string)

  const rows = table.getFilteredRowModel().rows.map((row) =>
    row.getVisibleCells().map((cell) => {
      const value = cell.getValue()
      return typeof value === 'string' && value.includes(',') ? `"${value}"` : value
    })
  )

  const csv = [headers, ...rows].map((r) => r.join(',')).join('\n')
  const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = `orders-${Date.now()}.csv`
  a.click()
  URL.revokeObjectURL(url)
}

\uFEFF — BOM для коректного відображення кирилиці в Excel.

Віртуалізація рядків

Для таблиць з тисячами рядків на клієнті — TanStack Virtual:

npm install @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual'

const tableContainerRef = useRef<HTMLDivElement>(null)
const { rows } = table.getRowModel()

const rowVirtualizer = useVirtualizer({
  count: rows.length,
  getScrollElement: () => tableContainerRef.current,
  estimateSize: () => 48, // висота рядка в px
  overscan: 10,
})

// У JSX:
<div ref={tableContainerRef} style={{ height: '600px', overflow: 'auto' }}>
  <table>
    <tbody style={{ height: `${rowVirtualizer.getTotalSize()}px`, position: 'relative' }}>
      {rowVirtualizer.getVirtualItems().map((virtualRow) => {
        const row = rows[virtualRow.index]
        return (
          <tr
            key={row.id}
            style={{
              position: 'absolute',
              top: 0,
              transform: `translateY(${virtualRow.start}px)`,
              height: `${virtualRow.size}px`,
            }}
          >
            {row.getVisibleCells().map((cell) => (
              <td key={cell.id}>
                {flexRender(cell.column.columnDef.cell, cell.getContext())}
              </td>
            ))}
          </tr>
        )
      })}
    </tbody>
  </table>
</div>

10 000 рядків рендерятся за ~20 мс — в DOM присутствує лише те, що видно у viewport.

Що робимо

Аналізуємо структуру даних та вимоги: обсяг, частота оновлень, потрібний ли inline-edit, експорт, фіксовані колонки. Під задачу вибираємо клієнтську або серверну модель пагінації, налаштовуємо колонки, сортування, фільтрацію. Стилізуємо під дизайн-систему проекту — таблиця виглядає як частина інтерфейсу, а не вставка з іншого додатку.

Термін: базова таблиця зі сортуванням та пагінацією — 1 день. З серверною пагінацією, фільтрами та експортом — 2–3 дні.