EXEPERTAI LAB

Research alpha

Time Machine
EXEPERTAI LAB
GALLERY / COLLECTION
← Browse Astryx gallery
Table·hook·@astryxdesign/core

useTableGroupedRows

Hook that groups a flat data array into collapsible section rows. Each distinct groupBy value becomes a full-width section-header row with a chevron toggle, the group label, and a member count; collapsing hides that group's data rows while keeping the header visible. Mirrors useTableTreeState: the consumer owns the collapsedGroups set and the hook returns {data, plugin, idKey}: pass them to Table as data, plugins, and idKey respectively. Grouping runs on the rows you hand it, so with pagination the order is filter, sort, slice, then group: sort by the group key first and the user's keys second, so a section's rows stay contiguous and each page appends to the bottom of the table instead of splicing rows in above the reader. A page that ends on a row count still cuts mid-section, and the heading then counts what has loaded rather than what exists: "6" quietly becoming "10". Where the full result set is in hand, carry the cut forward to the end of the section it lands in: every rendered section is then whole and its count is a total.

Open in Playground @astryxdesign/core/Table

Signature

Call shape
useTableGroupedRows()

Showcases and examples

1 documented example

useTableGroupedRows — Collapsible Groups

A table grouped into collapsible sections with useTableGroupedRows. Each group gets a full-width header with a chevron, label, and member count; click to collapse/expand.

Preview loads on approachPreview loads on approach
Exact source · table-grouped-rows-table
// Copyright (c) Meta Platforms, Inc. and affiliates.

'use client';

import {useState, useCallback} from 'react';
import {
  Table,
  useTableGroupedRows,
  proportional,
  pixel,
} from '@astryxdesign/core/Table';
import type {TableColumn} from '@astryxdesign/core/Table';

interface Person extends Record<string, unknown> {
  id: string;
  name: string;
  team: string;
  role: string;
}

const people: Person[] = [
  {id: '1', name: 'Ava Chen', team: 'Design Systems', role: 'Staff Eng'},
  {id: '2', name: 'Liam Park', team: 'Design Systems', role: 'Engineer'},
  {id: '3', name: 'Zoe Vega', team: 'Design Systems', role: 'Manager'},
  {id: '4', name: 'Max Ross', team: 'Infra', role: 'Senior Eng'},
  {id: '5', name: 'Mia Cole', team: 'Infra', role: 'Engineer'},
  {id: '6', name: 'Leo Nash', team: 'Growth', role: 'PM'},
];

const columns: TableColumn<Person>[] = [
  {key: 'name', header: 'Name', width: proportional(2)},
  {key: 'role', header: 'Role', width: pixel(140)},
];

export default function TableGroupedRowsTable() {
  const [collapsedGroups, setCollapsed] = useState<Set<string>>(new Set());
  const onToggleGroup = useCallback((key: string) => {
    setCollapsed(prev => {
      const next = new Set(prev);
      if (next.has(key)) {
        next.delete(key);
      } else {
        next.add(key);
      }
      return next;
    });
  }, []);

  const grouped = useTableGroupedRows<Person>({
    data: people,
    groupBy: p => p.team,
    collapsedGroups,
    onToggleGroup,
    getRowKey: p => p.id,
  });

  return (
    <Table
      data={grouped.data}
      columns={columns}
      idKey={grouped.idKey}
      hasHover
      plugins={{grouped: grouped.plugin}}
    />
  );
}

Usage

Hook that groups a flat data array into collapsible section rows. Each distinct groupBy value becomes a full-width section-header row with a chevron toggle, the group label, and a member count; collapsing hides that group's data rows while keeping the header visible. Mirrors useTableTreeState: the consumer owns the collapsedGroups set and the hook returns {data, plugin, idKey}: pass them to Table as data, plugins, and idKey respectively. Grouping runs on the rows you hand it, so with pagination the order is filter, sort, slice, then group: sort by the group key first and the user's keys second, so a section's rows stay contiguous and each page appends to the bottom of the table instead of splicing rows in above the reader. A page that ends on a row count still cuts mid-section, and the heading then counts what has loaded rather than what exists: "6" quietly becoming "10". Where the full result set is in hand, carry the cut forward to the end of the section it lands in: every rendered section is then whole and its count is a total.

Typed props

PropType and behavior
dataT[] · required
The flat data to group.
groupBy(item: T) => string · required
Derive the group key for a row. Rows with the same key share a section.
collapsedGroupsSet<string> · required
Set of currently-collapsed group keys.
onToggleGroup(groupKey: string) => void · required
Called with a group key when its header is toggled.
renderGroupHeader(groupKey: string, count: number, collapsed: boolean) => ReactNode
Custom renderer for a group header's content (right of the chevron). Defaults to <groupKey> (<count>).
getRowKey(item: T) => string
Stable key for a real row. Falls back to a positional key when omitted.
groupOrderstring[]
Explicit group ordering; groups not listed keep first-seen order after these.