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 отображают индикатор сортировки; фактическое упорядочивание выполняет бэкенд.

Интеграция с бэкендом

Типичный контракт на стороне приложения:

  1. Хранить items, total, loading, offset, limit, search, sorting в state.
  2. В useEffect (или data-fetch хуке) запрашивать страницу при изменении offset / limit / search / sorting.
  3. В onChangePage обновлять offset и limit; в search.onChange и sorting.onChange — сбрасывать offset в 0.
  4. При параллельных запросах отбрасывать устаревшие ответы (счётчик requestId / AbortController) — иначе медленный ответ может перезаписать актуальные данные.
  5. Пробрасывать 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'

Примеры использования

Серверный сценарий

Серверный сценарийИмитация бэкенда: `onChangePage`, controlled-поиск и сортировка с `manualSorting`, сброс `offset` при фильтрах, отмена устаревших ответов через `requestId`.
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

PropsServerTableProps
PropTypeDefaultRequiredDescription
autoResetPageIndexbooleannoАвтоматический сброс пагинации к первой странице при изменении данных/фильтров/сортировки
bulkActionsBulkAction[]noСписок действий для массовых операций
cardColumnsnumbernoЖелаемое число колонок карточного вида (`view='cards'`). На широком контейнере рисуется ровно столько колонок; при сужении сетка схлопывается до меньшего числа (порог — `cardMinWidth`). Без пропа число колонок определяется только шириной контейнера и `cardMinWidth` (auto-fill).
cardMinWidthnumber320noМинимальная ширина карточки в `view='cards'`, px. Порог, ниже которого колонки схлопываются. Карточка ужимается до ширины контейнера, если он уже.
classNamestringnoCSS-класс
columnDefinitionsColumnDefinition<TData>[]yesОпределение внешнего вида и функционала колонок
columnFilters(Omit<ChipChoiceRowProps<TFilters>, "data-test-id" | "size"> & { open?: boolean; initialOpen?: boolean; onOpenChange?(isOpen: boolean): void; } & { ...; }) | undefinednoФильтры
columnVirtualizerInstanceRefMutableRefObject<ColumnVirtualizer>noRef на инстанс column-virtualizer'а для управления прокруткой снаружи
columnVirtualizerOptionsPartial<VirtualizerOptions<HTMLElement, Element>>noДополнительные параметры column-virtualizer'а (`@tanstack/react-virtual`). Переопределяют дефолты (overscan=3).
columnsSettings{ enableDrag?: boolean; enableSettingsMenu?: boolean; } | undefinednoНастройки колонок: `enableDrag` — переупорядочивание (заголовки таблицы и строки в меню настроек); `enableSettingsMenu` — меню показа колонок.
copyPinnedRowsbooleanfalsenoПараметр отвечает за сохранение закрепленных строк в теле таблицы
data-test-idstringno
dataErrorbooleannoФлаг, показывающий что произошла ошибка запроса при пустых данных
dataFilteredbooleannoФлаг, показывающий что данные были отфильтрованы при пустых данных
defaultView"cards" | "table"'table' (на mobile — `cards`)noНачальный режим отображения (uncontrolled). Если не задан — дефолт по раскладке: `table` на desktop, `cards` на mobile (`TABLE_LAYOUT_PRESETS`).
enableColumnVirtualizationbooleanfalsenoВключает виртуализацию колонок (windowing по горизонтали). Рекомендуется при > 30 видимых колонок. Несовместимо с `view='cards'`. Pinned-колонки (left/right) всегда отрисовываются вне зависимости от настройки.
enableFuzzySearchbooleannoВключить нечеткий поиск
enableRowVirtualizationbooleanfalsenoВключает виртуализацию строк (windowing по вертикали). Рекомендуется при > 200 строк. Несовместимо с `view='cards'` — при картах игнорируется.
enableSelectPinnedbooleannoПараметр отвечает за чекбокс выбора закрепленных строк
errorDataStateEmptyStatePropsnoЭкран при ошибке запроса
expanding{ getSubRows: (element: TData) => TData[]; expandingColumnDefinition: TreeColumnDefinitionProps<TData>; initialState?: ExpandedState; state?: ExpandedState | undefined; onChange?(state: ExpandedState): void; } | undefinednoОбщие настройки раскрывающихся (tree) строк: `getSubRows`, `expandingColumnDefinition`, `initialState`, `state`, `onChange`.
fullWidthbooleantruenoРастягивать таблицу на всю ширину контейнера. При `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Функция получения уникального идентификатора строки
hasMoreundefinedno
headerRowBackgroundColor"blue" | "green" | "neutral" | "orange" | "pink" | "red" | "violet" | "yellow"noAccent-тон фона строки заголовков колонок (`tableHeadLine`). Работает только в `view='table'`.
headlineIdstringnoId колонки, чей рендер используется как заголовок карточки в режиме `view='cards'`. Имеет смысл только при `view='cards'`.
infiniteLoadingundefinedno
itemsTData[]noДанные для отрисовки
keepPinnedRowsbooleanfalsenoПараметр отвечает за отображение закрепленных строк на всех страницах таблицы
layoutPresetsPartial<Record<LayoutType, Partial<TableLayoutDefaults>>>noOverride дефолтов адаптива для этого инстанса (`mergePresets` поверх `TABLE_LAYOUT_PRESETS`). `stickyControls` в пресете tier'а заменяет DS-объект целиком — указывайте все нужные поля. Escape-hatch: обычно не нужен — DS-пресет применяется автоматически по `AdaptiveProvider`.
limitnumber10noКол-во строк на страницу
loadMoreTriggerundefinedno
loadingbooleannoСостояние загрузки
manualFilteringbooleantrueno
manualPaginationbooleantrueno
manualSortingbooleantrueno
moreActionsAction[]noЭлементы выпадающего списка кнопки с действиями
noDataStateEmptyStatePropsnoЭкран при отсутствии данных
noResultsStateEmptyStatePropsnoЭкран при отсутствии результатов поиска или фильтров
offsetnumber0noСмещение
onChangePage(offset: number, limit: number) => voidyes
onExport(() => void)noКолбэк экспорта данных. Рендерит иконку в тулбаре перед настройками колонок.
onLoadMoreundefinedno
onRefresh(() => void)noКолбэк обновления данных
onRowClickRowClickHandler<TData>noКолбэк клика по строке
onViewChange((view: View) => void)noКолбэк на смену режима отображения
outlinebooleannoВнешний бордер для тулбара и таблицы
pagination{ options?: number[]; optionsLabel?: string; } | undefinednoПараметры пагинации: `options`, `optionsLabel`
renderCard((context: RenderCardContext<TData>) => ReactNode)noКастомный рендер карточки в `view='cards'`. Получает контекст с tanstack `row` / `table` и `defaultRender` (готовый элемент дефолтной карточки — можно обернуть). Возврат заменяет дефолтную карточку.
rowAutoHeightbooleanno
rowPinningPick<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; } | undefinednoПараметры выбора строк: `initialState`, `state`, `enable`, `appearance`, `multiRow`, `onChange`.
rowVirtualizerInstanceRefMutableRefObject<RowVirtualizer>noRef на инстанс row-virtualizer'а для управления прокруткой снаружи
rowVirtualizerOptionsPartial<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; }) | undefinednoКонфиг сохранения состояния в localStorage и queryParams. `id` должен быть уникальным для разных таблиц в рамках приложения.
scrollContainerRefRefObject<HTMLElement>noСсылка на контейнер, который скроллится
scrollRefRef<HTMLElement>noСсылка на элемент, обозначающий самый конец прокручиваемого списка
search{ initialState?: string; state: string; placeholder?: string; loading?: boolean | undefined; onChange(value: string): void; } | undefinednoПараметры глобального поиска: `initialState`, `state`, `placeholder`, `loading`, `onChange`.
showDataViewbooleanfalsenoПоказывать переключатель вида (таблица/карточки) в тулбаре. Управляет только видимостью тоггла; сам вид задаётся `view` / `defaultView`. По умолчанию тоггла нет — таблица показывает один вид (`defaultView` либо адаптивный дефолт). Включите `showDataView`, чтобы дать пользователю переключать table/cards.
sorting{ initialState?: SortingState; state?: SortingState; onChange?(state: SortingState): void; } | undefinednoПараметры отвечают за возможность сортировки: `initialState` — начальное состояние; `state` — управляемое снаружи; `onChange` — колбэк на изменение.
stickyControlsStickyControlsnoSticky-хром при скролле страницы: при `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.
suppressHeaderbooleannoОтключение хедера таблицы; в режиме `view='cards'` скрывает подписи-заголовки полей карточки
suppressPaginationbooleannoОтключение пагинации
suppressSearchbooleannoОтключение поиска
suppressToolbarbooleannoОтключение тулбара
toolbarAfterReactNodenoДополнительный слот в `Toolbar` после строки поиска
totalnumber10noОбщее кол-во строк
view"cards" | "table"'table' (на mobile — `cards`)noРежим отображения таблицы (controlled). `table` — классическая сетка; `cards` — карточки (заголовок берётся из колонки `headlineId`). Переключатель вида в тулбаре включается отдельным пропом `showDataView`.

Unions

Types

ServerTableProps

Unions

Storybook

Figma

Смотри также

  • Обзор пакетаServerSimpleTable, ServerAdminTable и выбор компонента.
  • Table — клиентская таблица для данных, загруженных целиком.
  • SimpleTable — клиентский аналог ServerSimpleTable.
  • AdminTable — клиентский аналог ServerAdminTable.