Реалізація інтерактивних таблиць (TanStack Table) на сайті
Уявіть: ваш інтерфейс управління замовленнями починає гальмувати вже на 1000 рядків, а клієнти скаржаться на довге завантаження сторінки. Ми стикалися з цим десятки разів — і знаємо, як виправити. Таблиця з сортуванням і пагінацією — одне з найчастіших завдань у фронтенді, і одне з найчастіше реалізованих погано. jQuery DataTables зустрічається в легасі, але в сучасних React-додатках стандарт де-факто — TanStack Table (раніше react-table). Це headless-бібліотека без вбудованих стилів, що дає повний контроль над розміткою. Розмір ~14 КБ gzipped, з Tree-shaking ще менше. Активно підтримується, має TypeScript-типи та сумісна з React Server Components у Next.js. Якщо ви використовуєте Vue, Solid або Svelte — є адаптовані версії. За даними GitHub, проєкт має понад 30 000 зірок.
Проблеми, які вирішуємо
Більшість самописних таблиць страждають від кількох типових помилок:
- Відсутність віртуалізації — при 5000 рядків DOM перевантажується, React фризить на 2–3 секунди.
- Клієнтська пагінація на великих датасетах — завантаження 50 000 рядків убиває пропускну здатність мережі та пам'ять браузера.
- Немає debounce при фільтрації — на кожне натискання клавіші летить запит, сервер захлинається.
TanStack Table вирішує це на рівні бібліотеки: ви обираєте лише потрібні функції та конфігуруєте поведінку. Вона не нав'язує модель даних — ви самі визначаєте, як зберігати та завантажувати рядки. Перехід з jQuery DataTables на TanStack Table може скоротити час розробки на 30%, що знижує бюджет проєкту.
Порівняння бібліотек для таблиць
| Бібліотека | Розмір | Гнучкість | Продуктивність | Ком'юніті |
|---|---|---|---|---|
| TanStack Table v8 | ~14 КБ gzipped | Повна (headless) | Висока (віртуалізація) | >30K GitHub stars |
| jQuery DataTables | ~80 КБ (з jQuery) | Низька (стилі в комплекті) | Середня (нема віртуалізації) | Застаріла |
| AG Grid Community | ~200 КБ gzipped | Висока (багато функцій) | Дуже висока | >10K GitHub stars |
TanStack Table — оптимальний вибір для React/Vue/Solid/Svelte. AG Grid виправданий лише при необхідності Excel-режиму редагування на 100 000+ рядків. jQuery DataTables — виключно для підтримки легасі. За даними тестів, TanStack Table в 10 разів швидший за jQuery DataTables при роботі з 10 000 рядків завдяки віртуалізації та відсутності зайвих DOM-операцій.
Чому TanStack Table?
Він headless: жодних стилів у комплекті, лише логіка. Ви самі вирішуєте, як виглядає таблиця — ідеально вписується в дизайн-систему. Розмір ~14 КБ gzipped, з Tree-shaking ще менше. З версії 8 підтримує TypeScript та Server Components — у Next.js працює без проблем. Все, що потрібно — передати дані та колонки, а хук useReactTable сам керує станом сортування, фільтрації та пагінації.
Базова реалізація на React
Установка:
npm install @tanstack/react-table
Приклад компонента таблиці з пошуком, сортуванням та пагінацією
import { useState } from 'react'
import {
ColumnDef,
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
}
function OrdersTable({ data }: { data: Order[] }) {
const [sorting, setSorting] = useState<SortingState>([])
const [globalFilter, setGlobalFilter] = useState('')
const columns: ColumnDef<Order>[] = [
{
accessorKey: 'id',
header: '№ замовлення',
cell: info => <span className='font-mono text-sm'>{info.getValue()}</span>,
},
{ accessorKey: 'customer', header: 'Клієнт', enableSorting: true },
{
accessorKey: 'amount',
header: 'Сума',
cell: info => `${info.getValue<number>().toLocaleString('uk-UA')} ₴`,
sortingFn: 'basic',
},
{ accessorKey: 'status', header: 'Статус', cell: info => <StatusBadge status={info.getValue()} />, enableSorting: false },
{ accessorKey: 'createdAt', header: 'Дата', sortingFn: 'datetime' },
]
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>
)
}
Як реалізувати серверну пагінацію з React Query?
Для датасетів від 10 000 рядків клієнтська пагінація неприйнятна — всі дані не завантажити. Використовуємо manualPagination, manualSorting, manualFiltering та завантажуємо дані через React Query:
const [{ pageIndex, pageSize }, setPagination] = useState({ pageIndex: 0, pageSize: 25 })
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,
})
Ключове — прапорці manual* — вони вимикають вбудовану обробку, передаючи управління серверу. React Query з keepPreviousData: true не дає інтерфейсу смикатися при завантаженні.
Порівняння моделей пагінації
| Параметр | Клієнтська | Серверна |
|---|---|---|
| Розмір даних | до 10 000 рядків | будь-яка кількість |
| Час початкового завантаження | залежить від датасету | миттєво (1 сторінка) |
| Фільтрація | на клієнті (offline) | на сервері (SQL індекси) |
| Навантаження на сервер | одне велике завантаження | багато маленьких запитів |
Як експортувати таблицю в CSV та Excel?
Виводимо відфільтровані дані в CSV з BOM для кирилиці:
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)
}
При необхідності — експорт у XLSX через exceljs. Налаштовуємо формат комірок, об'єднання, стилі.
Віртуалізація для 100 000+ рядків
Коли рядків багато, але пагінація клієнтська — віртуалізація рядків через @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,
overscan: 10,
})
10 000 рядків рендеряться за ~20 мс — у DOM потрапляє лише viewport.
Що входить у роботу
- Аналіз структури даних та API, вибір моделі пагінації.
- Проектування конфігурації колонок, сортування, фільтрів.
- Реалізація на TanStack Table з інтеграцією в дизайн-систему.
- Налаштування пагінації на сервері, експорту, віртуалізації.
- Документація щодо використання та підтримки.
Терміни: базова таблиця з сортуванням та пагінацією — 1 день (від 300 $). Із серверною пагінацією, фільтрами та експортом — 2–3 дні (від 800 $). Для віртуалізації додаємо 1–2 дні. Оцінюємо проєкт безкоштовно — напишіть нам. Замовте розробку таблиці під ключ — отримайте готове рішення за 1 день. Отримайте консультацію щодо впровадження TanStack Table у ваш проєкт.
Ми займаємося розробкою фронтенду вже понад 5 років, реалізували 50+ проєктів з таблицями. Використовуємо сучасний стек: React, TypeScript, TanStack Table, React Query. Результат — швидкі, гнучкі таблиці, які не гальмують на великих даних. Економія на ліцензуванні порівняно з AG Grid може досягати 40% бюджету.
Гарантія — виправляємо баги безкоштовно протягом 30 днів після здачі. Зв'яжіться з нами — розрахуємо ваш проєкт за один день.







