Мы часто сталкиваемся с запросами на создание интерактивных таблиц для B2B-приложений. Основная боль — производительность при фильтрации, сортировке и группировке тысяч строк. Расскажем, как мы решаем эту задачу с помощью TanStack Table — headless-библиотеки, которая даёт полный контроль над UI и поведением. За 5 лет опыта мы реализовали более 30 проектов с таблицами данных, и TanStack Table — наш выбор по умолчанию. Один из проектов — CRM-система с таблицей заказов на 50 000 строк. Клиентская сортировка приводила к фризам на 10 секунд. Мы предложили server-side обработку — проблема решилась.
Как реализовать интерактивные таблицы с фильтрацией, сортировкой и группировкой?
TanStack Table v8 — современное решение для React. Установка: npm install @tanstack/react-table. Основные концепции: useReactTable принимает конфигурацию с моделями (getCoreRowModel, getSortedRowModel, getFilteredRowModel, getGroupedRowModel, getExpandedRowModel, getPaginationRowModel). Это позволяет быстро добавить функциональность без boilerplate. Для больших таблиц используем react-virtual для виртуализации строк — это снижает нагрузку на DOM.
Пошаговая инструкция:
- Определите интерфейс данных, например
Order. - Создайте колонки через
createColumnHelper. - Настройте состояние:
sorting,columnFilters,grouping,pagination. - Подключите модели строк.
- Рендерите таблицу: хедеры с сортировкой, фильтры для колонок, тело с группировкой и агрегацией, пагинацию.
Пример: Таблица заказов с группировкой и фильтрацией — интерактивные таблицы на
import {
useReactTable, getCoreRowModel, getSortedRowModel,
getFilteredRowModel, getGroupedRowModel, getExpandedRowModel,
getPaginationRowModel, flexRender, createColumnHelper
} from '@tanstack/react-table';
interface Order {
id: string;
customerName: string;
status: OrderStatus;
total: number;
category: string;
createdAt: Date;
}
const columnHelper = createColumnHelper<Order>();
const columns = [
columnHelper.accessor('id', {
header: '№',
size: 80,
cell: ({ getValue }) => <code className="text-xs">{getValue().slice(0, 8)}</code>
}),
columnHelper.accessor('customerName', {
header: 'Покупатель',
filterFn: 'includesString'
}),
columnHelper.accessor('status', {
header: 'Статус',
cell: ({ getValue }) => <StatusBadge status={getValue()} />,
filterFn: (row, columnId, filterValue) =>
filterValue.includes(row.getValue(columnId))
}),
columnHelper.accessor('total', {
header: 'Сумма',
cell: ({ getValue }) => formatCurrency(getValue()),
sortDescFirst: true,
aggregationFn: 'sum',
aggregatedCell: ({ getValue }) => (
<strong className="font-semibold">{formatCurrency(getValue<number>())}</strong>
)
}),
columnHelper.accessor('category', {
header: 'Категория'
}),
columnHelper.accessor('createdAt', {
header: 'Дата',
cell: ({ getValue }) => format(getValue(), 'dd.MM.yyyy HH:mm'),
sortingFn: 'datetime'
})
];
function OrdersTable({ data }) {
const [sorting, setSorting] = useState([{ id: 'createdAt', desc: true }]);
const [columnFilters, setColumnFilters] = useState([]);
const [globalFilter, setGlobalFilter] = useState('');
const [grouping, setGrouping] = useState<string[]>([]);
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 });
const table = useReactTable({
data,
columns,
state: { sorting, columnFilters, globalFilter, grouping, pagination },
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
onGroupingChange: setGrouping,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getGroupedRowModel: getGroupedRowModel(),
getExpandedRowModel: getExpandedRowModel(),
getPaginationRowModel: getPaginationRowModel()
});
return (
<div>
{/* Глобальный поиск */}
<input
value={globalFilter}
onChange={e => setGlobalFilter(e.target.value)}
placeholder="Поиск по всем полям..."
className="mb-4 px-3 py-2 border rounded-lg w-64"
/>
{/* Группировка */}
<div className="flex gap-2 mb-4">
<span className="text-sm text-gray-600">Группировка:</span>
{['status', 'category'].map(col => (
<button
key={col}
onClick={() => setGrouping(prev =>
prev.includes(col) ? prev.filter(c => c !== col) : [...prev, col]
)}
className={`text-xs px-2 py-1 rounded border ${
grouping.includes(col) ? 'bg-blue-50 border-blue-300' : 'border-gray-200'
}`}
>
{col}
</button>
))}
</div>
{/* Таблица */}
<div className="overflow-x-auto rounded-lg border border-gray-200">
<table className="w-full text-sm">
<thead className="bg-gray-50">
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id}
className="px-4 py-3 text-left font-medium text-gray-600 whitespace-nowrap"
style={{ width: header.getSize() }}
>
<div className="flex items-center gap-1">
<span
className={header.column.getCanSort() ? 'cursor-pointer select-none' : ''}
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(header.column.columnDef.header, header.getContext())}
</span>
{header.column.getIsSorted() === 'asc' && ' ↑'}
{header.column.getIsSorted() === 'desc' && ' ↓'}
</div>
{/* Фильтр для каждой колонки */}
{header.column.getCanFilter() && (
<ColumnFilter column={header.column} />
)}
</th>
))}
</tr>
))}
</thead>
<tbody className="divide-y divide-gray-100">
{table.getRowModel().rows.map(row => (
<tr key={row.id}
className={`hover:bg-gray-50 ${row.getIsGrouped() ? 'bg-gray-50 font-medium' : ''}`}>
{row.getVisibleCells().map(cell => (
<td key={cell.id} className="px-4 py-3">
{cell.getIsGrouped() ? (
<button onClick={row.getToggleExpandedHandler()} className="flex items-center gap-1">
{row.getIsExpanded() ? '▼' : '▶'}
{flexRender(cell.column.columnDef.cell, cell.getContext())}
({row.subRows.length})
</button>
) : cell.getIsAggregated() ? (
flexRender(cell.column.columnDef.aggregatedCell ?? cell.column.columnDef.cell, cell.getContext())
) : flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
{/* Пагинация */}
<TablePagination table={table} />
</div>
);
}
Почему стоит выбрать server-side обработку для больших таблиц?
При работе с более чем 10 000 строк клиентская обработка становится медленной — LCP растёт, пользователи испытывают фризы. Server-side сортировка и фильтрация решают проблему: данные загружаются порциями, а сортировка выполняется на сервере. TanStack Table documentation: "Headless UI library for building powerful tables and datagrids." Пример реализации:
function ServerSideTable() {
const [sorting, setSorting] = useState([]);
const [columnFilters, setColumnFilters] = useState([]);
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 });
const { data, isLoading } = useQuery({
queryKey: ['orders', { sorting, columnFilters, pagination }],
queryFn: () => fetch('/api/orders?' + new URLSearchParams({
sort: sorting.map(s => `${s.id}:${s.desc ? 'desc' : 'asc'}`).join(','),
filters: JSON.stringify(columnFilters),
page: String(pagination.pageIndex + 1),
per_page: String(pagination.pageSize)
})).then(r => r.json())
});
const table = useReactTable({
data: data?.items ?? [],
rowCount: data?.total ?? 0,
manualSorting: true,
manualFiltering: true,
manualPagination: true,
// ...
});
}
Сравнение подходов: client-side vs server-side
| Критерий | Client-side | Server-side |
|---|---|---|
| Количество строк | до 10 000 | от 10 000 |
| Скорость сортировки | мгновенно | с задержкой сети |
| Фильтрация | все строки | запрос к API |
| Группировка | на клиенте | на сервере |
| Нагрузка на браузер | высокая | низкая |
Для таблиц с объёмом данных до 1 000 строк client-side обычно быстрее, поскольку не требует сетевых запросов. При росте данных server-side становится единственно возможным вариантом — он в 5-10 раз снижает время отклика интерфейса.
Детальный анализ производительности: при 100 000 строк client-side рендеринг может занимать более 5 секунд, в то время как server-side с пагинацией по 50 строк загружается за 0.2 сек. TanStack Table с virtualized rows (через react-virtual) позволяет рендерить только видимые строки, что ещё больше ускоряет интерфейс. В отличие от коммерческих решений, TanStack Table — бесплатная open-source библиотека, что существенно экономит бюджет проекта.
Пример конфигурации агрегации для группировки
Для корректного отображения сумм в сгруппированных строках нужно задать aggregationFn для колонки. Например, для колонки total используем aggregationFn: 'sum'. Можно также определить кастомную функцию.
Какие преимущества даёт группировка строк в интерактивной таблице?
Группировка позволяет объединять строки по значению колонки, например, по статусу или категории. Это упрощает анализ данных: сразу видно количество записей в каждой группе и агрегированные значения (сумма, среднее и т.д.). В TanStack Table группировка реализуется через getGroupedRowModel и getExpandedRowModel, а пользователь может сворачивать и разворачивать группы. Группировка эффективна при работе с иерархическими данными и часто используется в системах аналитики.
Типичные ошибки при разработке таблиц с фильтрацией и сортировкой
- Игнорирование виртуализации: без react-virtual даже 5 000 строк могут вызвать задержки. Всегда подключайте
@tanstack/react-virtual. - Отсутствие debounce при фильтрации: каждый ввод символа вызывает перерисовку. Используйте
useDebounceс задержкой 300 мс. - Неправильная настройка колонок:
sizeиminSizeдолжны быть заданы, иначе таблица может разъезжаться. - Забытая агрегация при группировке: если не задать
aggregationFn, сгруппированные строки покажутundefined.
Что входит в разработку интерактивной таблицы под ключ?
- Аналитика: определение набора колонок, типов данных, требований к производительности.
- Проектирование: выбор архитектуры (client-side/server-side), UI-компонентов.
- Реализация: настройка сортировки, фильтрации, группировки, пагинации, экспорта, кастомных фильтров (date range, multiselect).
- Тестирование: проверка на больших объёмах данных, Edge-кейсы, оптимизация Core Web Vitals.
- Деплой и документация: размещение на сервере, описание API и инструкция для разработчиков.
- Обучение: передача знаний команде заказчика.
Мы гарантируем, что таблица будет работать с Core Web Vitals в норме (LCP < 2.5s, CLS < 0.1). Получите консультацию по вашему проекту — свяжитесь с нами.
Типовые сроки и этапы
| Этап | Длительность |
|---|---|
| Аналитика | 1-2 дня |
| Проектирование и прототип | 2-3 дня |
| Реализация core-функций | 5-7 дней |
| Интеграция с API (server-side) | 3-5 дней |
| Тестирование и оптимизация | 2-3 дня |
Сроки разработки
- Базовая таблица (сортировка, фильтры, пагинация, группировка): от 5 до 7 дней.
- Добавление server-side, экспорта в Excel/CSV, кастомных виджетов: ещё от 5 до 7 дней.
Стоимость рассчитывается индивидуально, зависит от сложности и объёма данных. Обращайтесь — подберём оптимальное решение.







