ServerTable
ServerTable — обёртка над Table для постраничных данных с бэкенда. Страница данных приходит снаружи (items / total / limit / offset), смена страницы, поиск и сортировка делегируются серверу: компонент вызывает onChangePage, search.onChange и sorting.onChange, а отрисовывает то, что передано в пропсах.
Для типовых серверных сценариев есть preset-ы ServerSimpleTable и ServerAdminTable (см. обзор пакета) — они маппят упрощённый API в ServerTable, как SimpleTable / AdminTable маппят в Table.
Когда использовать
- Данные приходят с бэкенда постранично, известно общее количество строк (
total). - Поиск и сортировка выполняются на сервере, а не на клиенте.
- Объём данных слишком велик для загрузки на клиент целиком.
Когда не нужен:
- Данные загружены на клиент целиком:
- используйте
TableилиSimpleTable— пагинация, поиск и сортировка работают локально.
- используйте
Рекомендации по выбору API:
- Простой серверный список (колонки + пагинация) —
ServerSimpleTable. - Админ-экран с поиском, статусом, выбором и действиями —
ServerAdminTable. - Нестандартные колонки, фильтры или поведение тулбара —
ServerTableнапрямую.
Анатомия
Постраничная загрузка (default limit 10, offset 0)
items— строки текущей страницы.total— общее количество строк; по нему рассчитывается число страниц.limit/offset— размер страницы и смещение текущей страницы.onChangePage(offset, limit)— вызывается при смене страницы или размера страницы; здесь выполняется запрос новой страницы.pagination.options/pagination.optionsLabel— варианты числа строк на страницу и подпись к ним.
Поиск (controlled)
search.stateиsearch.onChangeобязательны — значение поиска хранится снаружи, в отличие отTable.search.loading— индикатор загрузки в строке поиска.search.initialValue— начальное значение.- Ввод дебаунсится внутри компонента (500 мс, отдельный таймер на каждый инстанс) —
onChangeполучает значение после паузы ввода. При смене запроса обычно сбрасываютoffsetв0.
Серверная сортировка
- По умолчанию включён
manualSorting— таблица не сортируетitemsна клиенте. - Передайте управляемый
sorting.state+sorting.onChange; при смене сортировки запросите новую страницу и сбросьтеoffsetв0. - Колонки с
enableSorting: trueотображают индикатор сортировки; фактическое упорядочивание выполняет бэкенд.
Интеграция с бэкендом
Типичный контракт на стороне приложения:
- Хранить
items,total,loading,offset,limit,search,sortingв state. - В
useEffect(или data-fetch хуке) запрашивать страницу при измененииoffset/limit/search/sorting. - В
onChangePageобновлятьoffsetиlimit; вsearch.onChangeиsorting.onChange— сбрасыватьoffsetв0. - При параллельных запросах отбрасывать устаревшие ответы (счётчик
requestId/AbortController) — иначе медленный ответ может перезаписать актуальные данные. - Пробрасывать
loadingвServerTableи вsearch.loadingна время запроса.
Отличия от Table
- Данные передаются через
items(вместоdata) — только текущая страница. - Вырезаны
pageSize,pageCount,pagination.state,toolbarCheckBoxMode— пагинацией управляет бэкенд черезlimit/offset/total. - Серверная сортировка —
manualSorting+ управляемыйsorting.state/onChange. - Остальное API — колонки, служебные колонки, выбор строк, дерево, режим карточек, тулбар — наследуется от
Table.
Установка
pnpm add @cloud-ru/ds-table
import { ServerTable } from '@cloud-ru/ds-table'
Примеры использования
Серверный сценарий
tsx
import { ColumnDefinition, ServerTable, SortingState } from '@cloud-ru/ds-table';
import { useEffect, useRef, useState } from 'react';
type User = {
id: string;
name: string;
email: string;
role: string;
balance: number;
};
const NAMES = [
'Анна Иванова',
'Борис Петров',
'Вера Сидорова',
'Глеб Кузнецов',
'Дарья Орлова',
'Егор Морозов',
'Жанна Волкова',
'Захар Соколов',
];
const ROLES = ['Owner', 'Admin', 'Editor', 'Viewer'];
const ALL_USERS: User[] = Array.from({ length: 23 }, (_, index) => ({
id: `u-${index + 1}`,
name: NAMES[index % NAMES.length],
email: `user-${index + 1}@example.com`,
role: ROLES[index % ROLES.length],
balance: (index * 1730) % 20000,
}));
function compareUsers(a: User, b: User, columnId: string, desc: boolean): number {
const left = a[columnId as keyof User];
const right = b[columnId as keyof User];
const order =
typeof left === 'number' && typeof right === 'number' ? left - right : String(left).localeCompare(String(right));
return desc ? -order : order;
}
type PageResponse = {
items: User[];
total: number;
};
// Имитация бэкенда: фильтрация по имени, сортировка и срез по offset/limit с задержкой.
function fetchUsers(offset: number, limit: number, query: string, sorting: SortingState): Promise<PageResponse> {
return new Promise(resolve => {
setTimeout(() => {
const filtered = query
? ALL_USERS.filter(user => user.name.toLowerCase().includes(query.toLowerCase()))
: ALL_USERS;
const sortRule = sorting[0];
const sorted = sortRule ? [...filtered].sort((a, b) => compareUsers(a, b, sortRule.id, sortRule.desc)) : filtered;
resolve({ items: sorted.slice(offset, offset + limit), total: sorted.length });
}, 400);
});
}
const columns: ColumnDefinition<User>[] = [
{ accessorKey: 'name', header: 'Имя', enableSorting: true, size: 200 },
{ accessorKey: 'email', header: 'Email', size: 240 },
{ accessorKey: 'role', header: 'Роль', size: 140 },
{ accessorKey: 'balance', header: 'Баланс', align: 'right', headerAlign: 'right', enableSorting: true, size: 140 },
];
export function ServerDriven() {
const [items, setItems] = useState<User[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [offset, setOffset] = useState(0);
const [limit, setLimit] = useState(5);
const [search, setSearch] = useState('');
const [sorting, setSorting] = useState<SortingState>([]);
const requestId = useRef(0);
useEffect(() => {
const currentRequest = ++requestId.current;
setLoading(true);
fetchUsers(offset, limit, search, sorting).then(response => {
// Ответы устаревших запросов отбрасываются — состояние обновляет только последний.
if (currentRequest !== requestId.current) {
return;
}
setItems(response.items);
setTotal(response.total);
setLoading(false);
});
}, [offset, limit, search, sorting]);
return (
<ServerTable
items={items}
total={total}
limit={limit}
offset={offset}
loading={loading}
columnDefinitions={columns}
onChangePage={(nextOffset, nextLimit) => {
setOffset(nextOffset);
setLimit(nextLimit);
}}
search={{
state: search,
placeholder: 'Поиск по имени',
loading,
onChange: value => {
setOffset(0);
setSearch(value);
},
}}
sorting={{
state: sorting,
onChange: nextSorting => {
setOffset(0);
setSorting(nextSorting);
},
}}
manualSorting
pagination={{ options: [5, 10] }}
outline
/>
);
}Props
Types
Props
ServerTableProps| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
autoResetPageIndex | boolean | — | no | Автоматический сброс пагинации к первой странице при изменении данных/фильтров/сортировки |
bulkActions | BulkAction[] | — | no | Список действий для массовых операций |
cardColumns | number | — | no | Желаемое число колонок карточного вида (`view='cards'`). На широком контейнере рисуется ровно столько колонок; при сужении сетка схлопывается до меньшего числа (порог — `cardMinWidth`). Без пропа число колонок определяется только шириной контейнера и `cardMinWidth` (auto-fill). |
cardMinWidth | number | 320 | no | Минимальная ширина карточки в `view='cards'`, px. Порог, ниже которого колонки схлопываются. Карточка ужимается до ширины контейнера, если он уже. |
className | string | — | no | CSS-класс |
columnDefinitions | ColumnDefinition<TData>[] | — | yes | Определение внешнего вида и функционала колонок |
columnFilters | (Omit<ChipChoiceRowProps<TFilters>, "data-test-id" | "size"> & { open?: boolean; initialOpen?: boolean; onOpenChange?(isOpen: boolean): void; } & { ...; }) | undefined | — | no | Фильтры |
columnVirtualizerInstanceRef | MutableRefObject<ColumnVirtualizer> | — | no | Ref на инстанс column-virtualizer'а для управления прокруткой снаружи |
columnVirtualizerOptions | Partial<VirtualizerOptions<HTMLElement, Element>> | — | no | Дополнительные параметры column-virtualizer'а (`@tanstack/react-virtual`). Переопределяют дефолты (overscan=3). |
columnsSettings | { enableDrag?: boolean; enableSettingsMenu?: boolean; } | undefined | — | no | Настройки колонок: `enableDrag` — переупорядочивание (заголовки таблицы и строки в меню настроек); `enableSettingsMenu` — меню показа колонок. |
copyPinnedRows | boolean | false | no | Параметр отвечает за сохранение закрепленных строк в теле таблицы |
data-test-id | string | — | no | |
dataError | boolean | — | no | Флаг, показывающий что произошла ошибка запроса при пустых данных |
dataFiltered | boolean | — | no | Флаг, показывающий что данные были отфильтрованы при пустых данных |
defaultView | "cards" | "table" | 'table' (на mobile — `cards`) | no | Начальный режим отображения (uncontrolled). Если не задан — дефолт по раскладке: `table` на desktop, `cards` на mobile (`TABLE_LAYOUT_PRESETS`). |
enableColumnVirtualization | boolean | false | no | Включает виртуализацию колонок (windowing по горизонтали). Рекомендуется при > 30 видимых колонок. Несовместимо с `view='cards'`. Pinned-колонки (left/right) всегда отрисовываются вне зависимости от настройки. |
enableFuzzySearch | boolean | — | no | Включить нечеткий поиск |
enableRowVirtualization | boolean | false | no | Включает виртуализацию строк (windowing по вертикали). Рекомендуется при > 200 строк. Несовместимо с `view='cards'` — при картах игнорируется. |
enableSelectPinned | boolean | — | no | Параметр отвечает за чекбокс выбора закрепленных строк |
errorDataState | EmptyStateProps | — | no | Экран при ошибке запроса |
expanding | { getSubRows: (element: TData) => TData[]; expandingColumnDefinition: TreeColumnDefinitionProps<TData>; initialState?: ExpandedState; state?: ExpandedState | undefined; onChange?(state: ExpandedState): void; } | undefined | — | no | Общие настройки раскрывающихся (tree) строк: `getSubRows`, `expandingColumnDefinition`, `initialState`, `state`, `onChange`. |
fullWidth | boolean | true | no | Растягивать таблицу на всю ширину контейнера. При `false` ширина определяется суммой колонок (лучше всего, когда у всех колонок задан `size` / `width`). Явный проп = desktop-значение; на mobile всегда `true` (`TABLE_LAYOUT_PRESETS`). |
getRowBackgroundColor | ((data: TData) => TableRowColor) | — | no | Функция определения цвета фона строки по её данным. Работает только в `view='table'` — карточки (`view='cards'`) не тонируются. @param data данные строки @returns цвет фона строки или `undefined` |
getRowId | ((originalRow: TData, index: number, parent?: Row<TData>) => string) | — | no | Функция получения уникального идентификатора строки |
hasMore | undefined | — | no | |
headerRowBackgroundColor | "blue" | "green" | "neutral" | "orange" | "pink" | "red" | "violet" | "yellow" | — | no | Accent-тон фона строки заголовков колонок (`tableHeadLine`). Работает только в `view='table'`. |
headlineId | string | — | no | Id колонки, чей рендер используется как заголовок карточки в режиме `view='cards'`. Имеет смысл только при `view='cards'`. |
infiniteLoading | undefined | — | no | |
items | TData[] | — | no | Данные для отрисовки |
keepPinnedRows | boolean | false | no | Параметр отвечает за отображение закрепленных строк на всех страницах таблицы |
layoutPresets | Partial<Record<LayoutType, Partial<TableLayoutDefaults>>> | — | no | Override дефолтов адаптива для этого инстанса (`mergePresets` поверх `TABLE_LAYOUT_PRESETS`). `stickyControls` в пресете tier'а заменяет DS-объект целиком — указывайте все нужные поля. Escape-hatch: обычно не нужен — DS-пресет применяется автоматически по `AdaptiveProvider`. |
limit | number | 10 | no | Кол-во строк на страницу |
loadMoreTrigger | undefined | — | no | |
loading | boolean | — | no | Состояние загрузки |
manualFiltering | boolean | true | no | |
manualPagination | boolean | true | no | |
manualSorting | boolean | true | no | |
moreActions | Action[] | — | no | Элементы выпадающего списка кнопки с действиями |
noDataState | EmptyStateProps | — | no | Экран при отсутствии данных |
noResultsState | EmptyStateProps | — | no | Экран при от сутствии результатов поиска или фильтров |
offset | number | 0 | no | Смещение |
onChangePage | (offset: number, limit: number) => void | — | yes | |
onExport | (() => void) | — | no | Колбэк экспорта данных. Рендерит иконку в тулбаре перед настройками колонок. |
onLoadMore | undefined | — | no | |
onRefresh | (() => void) | — | no | Колбэк обновления данных |
onRowClick | RowClickHandler<TData> | — | no | Колбэк клик а по строке |
onViewChange | ((view: View) => void) | — | no | Колбэк на смену режима отображения |
outline | boolean | — | no | Внешний бордер для тулбара и таблицы |
pagination | { options?: number[]; optionsLabel?: string; } | undefined | — | no | Параметры пагинации: `options`, `optionsLabel` |
renderCard | ((context: RenderCardContext<TData>) => ReactNode) | — | no | Кастомный рендер карточки в `view='cards'`. Получает контекст с tanstack `row` / `table` и `defaultRender` (готовый элемент дефолтной карточки — можно обернуть). Возврат заменяет дефолтную карточку. |
rowAutoHeight | boolean | — | no | |
rowPinning | Pick<RowPinningState, "top"> | — | no | Определение, какие строки должны быть закреплены в таблице |
rowSelection | { initialState?: RowSelectionState; state?: RowSelectionState; enable?: boolean | ((row: Row<TData>) => boolean) | undefined; multiRow?: boolean | undefined; onChange?(state: RowSelectionState): void; appearance?: RowAppearance | undefined; } | undefined | — | no | Параметры выбора строк: `initialState`, `state`, `enable`, `appearance`, `multiRow`, `onChange`. |
rowVirtualizerInstanceRef | MutableRefObject<RowVirtualizer> | — | no | Ref на инстанс row-virtualizer'а для управления прокруткой снаружи |
rowVirtualizerOptions | Partial<VirtualizerOptions<HTMLElement, Element>> | — | no | Дополнительные параметры row-virtualizer'а (`@tanstack/react-virtual`). Переопределяют дефолты (overscan=10, estimateSize=40). |
savedState | (Pick<ToolbarPersistConfig<TFilters>, "serializer" | "parser"> & { id: string; filterQueryKey?: string; resize?: boolean; columnSettings?: boolean | undefined; }) | undefined | — | no | Конфиг сохранения состояния в localStorage и queryParams. `id` должен быть уникальным для разных таблиц в рамках приложения. |
scrollContainerRef | RefObject<HTMLElement> | — | no | Ссылка на контейнер, который скроллится |
scrollRef | Ref<HTMLElement> | — | no | Ссылка на элемент, обозначающий самый конец прокручиваемого списка |
search | { initialState?: string; state: string; placeholder?: string; loading?: boolean | undefined; onChange(value: string): void; } | undefined | — | no | Параметры глобального по иска: `initialState`, `state`, `placeholder`, `loading`, `onChange`. |
showDataView | boolean | false | no | Показывать переключатель вида (таблица/карточки) в тулбаре. Управляет только видимостью тоггла; сам вид задаётся `view` / `defaultView`. По умолчанию тоггла нет — таблица показывает один вид (`defaultView` либо адаптивный дефолт). Включите `showDataView`, чтобы дать пользователю переключать table/cards. |
sorting | { initialState?: SortingState; state?: SortingState; onChange?(state: SortingState): void; } | undefined | — | no | Параметры отвечают за возможность сортировки: `initialState` — начальное состояние; `state` — управляемое снаружи; `onChange` — колбэк на изменение. |
stickyControls | StickyControls | — | no | Sticky-хром при скролле страницы: при `enabled: true` тулбар и пагинация липнут к верху/низу viewport, в table-view заголовок колонок — под тулбаром; тело растёт по контенту. При `enabled: false` все блоки идут сплошным потоком без sticky. Дефолты: desktop — `enabled: false` (offsets не применяются); mobile — `{ enabled: true, offsetTop: 0, offsetBottom: 0 }` (`TABLE_LAYOUT_PRESETS`); `backgroundPredefined` — `neutralBackground1Level` на всех раскладках. Явный проп = desktop-значение; mobile-override — `layoutPresets.mobile`. @example `stickyControls={{ enabled: true, offsetTop: 64 }}` — sticky на desktop, app header 64px. |
suppressHeader | boolean | — | no | Отключение хедера таблицы; в режиме `view='cards'` скрывает подписи-заголовки полей карточки |
suppressPagination | boolean | — | no | Отключение пагинации |
suppressSearch | boolean | — | no | Отключение поиска |
suppressToolbar | boolean | — | no | Отключение тулбара |
toolbarAfter | ReactNode | — | no | Дополнительный слот в `Toolbar` после строки поиска |
total | number | 10 | no | Общее кол-во строк |
view | "cards" | "table" | 'table' (на mobile — `cards`) | no | Режим отображения таблицы (controlled). `table` — классическая сетка; `cards` — карточки (заголовок берётся из колонки `headlineId`). Переключатель вида в тулбаре включается отдельным пропом `showDataView`. |
Unions
Types
ServerTableProps
ColumnDefinition
EmptyStateProps
Except
RowClickHandler
TableLayoutDefaults
TreeColumnDefinitionProps
Unions
View
Related props
FilterRow
LayoutPresets
Storybook
Figma
Смотри также
- Обзор пакета —
ServerSimpleTable,ServerAdminTableи выбор компонента. - Table — клиентская таблица для данных, загруженных целиком.
- SimpleTable — клиентский аналог
ServerSimpleTable. - AdminTable — клиентский аналог
ServerAdminTable.