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







