Реалізація інтерактивних таблиць (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 днів після здачі. Зв'яжіться з нами — розрахуємо ваш проєкт за один день.







