Реализация интерактивных таблиц (TanStack Table) на сайте
Представьте: ваш интерфейс управления заказами начинает тормозить уже на 1000 строк, а клиенты жалуются на долгую загрузку страницы. Мы сталкивались с этим десятки раз — и знаем, как исправить. Таблица с сортировкой и пагинацией — одна из самых частых задач во фронтенде, и одна из самых часто реализуемых плохо. jQuery DataTables встречается в легаси, но в современных React-приложениях стандарт де-факто — TanStack Table (ранее react-table). Это headless-библиотека без встроенных стилей, дающая полный контроль над разметкой. Размер ~14 КБ gzipped, с Tree-shaking ещё меньше. Активно поддерживается, имеет TypeScript-типы и совместима с React Server Components в Next.js. Если вы используете Vue, Solid или Svelte — есть адаптированные версии.
Проблемы, которые решаем
Большинство самописных таблиц страдают от нескольких типовых ошибок:
- Отсутствие виртуализации — при 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 |
Это оптимальный выбор для React/Vue/Solid/Svelte. AG Grid оправдан только при необходимости Excel-режима редактирования на 100 000+ строк. jQuery DataTables — исключительно для поддержки легаси. Библиотека в 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('ru-RU')} ₽`,
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 день. С серверной пагинацией, фильтрами и экспортом — 2–3 дня. Для виртуализации добавляем 1–2 дня. Оцениваем проект бесплатно — напишите нам. Закажите разработку таблицы под ключ — получите готовое решение за 1 день. Получите консультацию по внедрению TanStack Table в ваш проект.
Мы занимаемся разработкой фронтенда 5+ лет, реализовали 50+ проектов с таблицами. Используем современный стек: React, TypeScript, TanStack Table, React Query. Результат — быстрые, гибкие таблицы, которые не тормозят на больших данных. Экономия на лицензировании по сравнению с AG Grid может достигать 40% бюджета.
Гарантия — исправляем баги бесплатно в течение 30 дней после сдачи. Свяжитесь с нами — рассчитаем ваш проект за один день.







