How to Use Astryx Table Components with Pagination, Selection, and Row Expansion
Astryx Table components utilize a composable plugin architecture where pagination, selection, and row expansion are implemented via independent hooks (useTablePagination, useTableSelection, useTableRowExpansion) that return plugin objects injected through the Table's plugins prop.
The Astryx design system (facebook/astryx) provides a flexible Table component that separates core rendering from feature-specific logic. Instead of monolithic configuration props, Astryx Table components leverage a plugin-based approach where each feature is encapsulated in its own hook, allowing developers to mix and match functionality while maintaining full control over state management.
Understanding the Astryx Table Plugin Architecture
Core table rendering lives in packages/core/src/Table/Table.tsx and packages/core/src/Table/BaseTable.tsx, while each feature is implemented as an independent hook returning a TablePlugin. The table accepts these plugins via its plugins prop, and each plugin's transformTableContext method injects the necessary UI around the table body.
All plugins follow this consistent pattern:
export function useTableXYZ<T extends Record<string, unknown>>(
config: XYZConfig,
): TablePlugin<T> {
return useMemo(() => ({
transformTableContext(children) {
// Render extra UI (Pagination, checkboxes, expand toggles) around children
return <>{/* UI */}{children}{/* UI */}</>;
},
}), []);
}
The Table component merges the plugins and base table context, then calls each plugin's transformTableContext in the order they appear in the plugins object.
Implementing Pagination with useTablePagination
The useTablePagination hook, located in packages/core/src/Table/plugins/pagination/useTablePagination.tsx, renders a Pagination component and handles navigation state. Note that this plugin manages only the UI controls; you must manually slice your data array to display the correct page.
import { Table } from '@astryxdesign/core';
import { useTablePagination } from '@astryxdesign/core';
import { useState, useMemo } from 'react';
const data = useMemo(() => Array.from({length: 125}, (_, i) => ({
id: i + 1,
name: `Item ${i + 1}`,
value: Math.round(Math.random() * 100),
})), []);
const [page, setPage] = useState(1);
const pageSize = 10;
const pageData = useMemo(
() => data.slice((page - 1) * pageSize, page * pageSize),
[data, page, pageSize],
);
const paginationPlugin = useTablePagination({
page,
onPageChange: setPage,
totalItems: data.length,
pageSize,
pageSizeOptions: [10, 25, 50],
onPageSizeChange: (size) => console.log('new size', size),
position: 'both', // Render pagination above and below
align: 'center',
});
export default function PaginatedTable() {
return (
<Table
data={pageData}
columns={[
{key: 'id', header: 'ID'},
{key: 'name', header: 'Name'},
{key: 'value', header: 'Value'},
]}
plugins={{pagination: paginationPlugin}}
/>
);
}
Key configuration options:
position: Controls pagination placement ('below','above','both', or'none')pageSizeOptions: Array of numbers defining selectable page sizesalign: Horizontal alignment of pagination controls ('left','center','right')
Adding Row Selection with useTableSelection
The useTableSelection hook in packages/core/src/Table/plugins/selection/useTableSelection.tsx adds a selectable column with checkboxes, maintains an array of selected row keys, and provides callbacks for change events.
import { Table } from '@astryxdesign/core';
import { useTableSelection } from '@astryxdesign/core';
import { useState, useMemo } from 'react';
const rows = useMemo(() => [
{id: 'a', title: 'Alpha'},
{id: 'b', title: 'Beta'},
{id: 'c', title: 'Gamma'},
], []);
const [selected, setSelected] = useState<string[]>([]);
const selectionPlugin = useTableSelection({
getRowKey: (row) => row.id,
selectedKeys: selected,
onSelectionChange: setSelected,
showSelectAll: true, // Renders "Select all" checkbox in header
});
export default function SelectableTable() {
return (
<Table
data={rows}
columns={[
{key: 'title', header: 'Title'},
]}
plugins={{selection: selectionPlugin}}
/>
);
}
Important implementation details:
getRowKey: Required function that receives the row object and returns a unique identifierselectedKeys: Controlled array of selected row keys; the plugin callsonSelectionChangewhenever the user toggles a row or the "Select all" header
Enabling Row Expansion with useTableRowExpansion
The useTableRowExpansion hook in packages/core/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx supplies an expandable toggle per row and lets you render custom expanded content via the renderExpandedRow callback.
import { Table } from '@astryxdesign/core';
import { useTableRowExpansion } from '@astryxdesign/core';
import { useState, useMemo } from 'react';
const rows = useMemo(() => [
{id: 1, name: 'Alice', details: 'Age: 30, Role: Engineer'},
{id: 2, name: 'Bob', details: 'Age: 28, Role: Designer'},
], []);
const [expanded, setExpanded] = useState<number[]>([]);
const expansionPlugin = useTableRowExpansion({
getRowKey: (row) => row.id,
expandedKeys: expanded,
onExpansionChange: setExpanded,
renderExpandedRow: (row) => (
<div style={{padding: '8px', background: '#fafafa'}}>
{row.details}
</div>
),
});
export default function ExpandableTable() {
return (
<Table
data={rows}
columns={[
{key: 'name', header: 'Name'},
]}
plugins={{rowExpansion: expansionPlugin}}
/>
);
}
The plugin automatically injects an expand/collapse toggle to the left of each row. The renderExpandedRow function receives the original row object and returns a React node that appears underneath the row when expanded.
Combining Pagination, Selection, and Row Expansion
You can compose multiple plugins by including them in the plugins prop object. When combining features, maintain separate state slices for each concern and pass the appropriately sliced data to the Table.
import { Table } from '@astryxdesign/core';
import {
useTablePagination,
useTableSelection,
useTableRowExpansion,
} from '@astryxdesign/core';
import { useState, useMemo } from 'react';
const allRows = useMemo(() => Array.from({length: 200}, (_, i) => ({
id: i,
name: `Item ${i}`,
details: `Detailed info for item ${i}`,
})), []);
const [page, setPage] = useState(1);
const pageSize = 20;
const pageData = useMemo(
() => allRows.slice((page - 1) * pageSize, page * pageSize),
[allRows, page, pageSize],
);
const [selected, setSelected] = useState<number[]>([]);
const [expanded, setExpanded] = useState<number[]>([]);
const pagination = useTablePagination({
page,
onPageChange: setPage,
totalItems: allRows.length,
pageSize,
position: 'both',
});
const selection = useTableSelection({
getRowKey: (row) => row.id,
selectedKeys: selected,
onSelectionChange: setSelected,
});
const expansion = useTableRowExpansion({
getRowKey: (row) => row.id,
expandedKeys: expanded,
onExpansionChange: setExpanded,
renderExpandedRow: (row) => (
<div style={{padding: '8px', background: '#f0f8ff'}}>
{row.details}
</div>
),
});
export default function FullFeaturedTable() {
return (
<Table
data={pageData}
columns={[
{key: 'name', header: 'Name'},
]}
plugins={{
pagination,
selection,
rowExpansion: expansion,
}}
/>
);
}
This configuration produces a table with pagination controls above and below the grid, checkboxes for row selection with "Select all" functionality, and expandable rows revealing custom detail content.
Key Source Files and Implementation Details
For further customization or debugging, reference these source files in the facebook/astryx repository:
- Core Table component:
packages/core/src/Table/Table.tsx - Base table implementation:
packages/core/src/Table/BaseTable.tsx - Pagination plugin:
packages/core/src/Table/plugins/pagination/useTablePagination.tsx - Selection plugin:
packages/core/src/Table/plugins/selection/useTableSelection.tsx - Row expansion plugin:
packages/core/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx - Table context:
packages/core/src/Table/TableContext.ts - Storybook examples:
apps/storybook/stories/TablePagination.stories.tsx,TableSelection.stories.tsx,TableRowExpansion.stories.tsx
Summary
- Astryx Tables use a plugin system where features are independent hooks returning
TablePluginobjects that transform the table context - The
pluginsprop accepts an object mapping feature names (likepagination,selection,rowExpansion) to plugin instances - Pagination requires manual data slicing; the plugin only renders controls and manages page navigation state
- Selection and expansion follow controlled patterns using
selectedKeysandexpandedKeysarrays withgetRowKeyfor unique identification - Plugins compose automatically through their
transformTableContextmethods, allowing flexible UI positioning without internal conflicts
Frequently Asked Questions
Does the pagination plugin handle data slicing automatically?
No. According to the implementation in useTablePagination.tsx, the plugin only renders the Pagination UI component. You must slice your data array manually based on the current page and page size before passing it to the Table's data prop, as shown in the code examples above.
Can I use row selection and row expansion together in the same table?
Yes. The plugin architecture in Table.tsx supports multiple plugins simultaneously. Simply include both the selection and rowExpansion plugins in the plugins prop object. Their transformTableContext methods execute in order, automatically arranging the UI controls (checkboxes and expand toggles) around the table body without conflicts.
How does Astryx identify individual rows for selection and expansion?
Both useTableSelection and useTableRowExpansion require a getRowKey function that receives the row object and returns a unique string or number identifier. This key is used to track selected or expanded state in the selectedKeys and expandedKeys arrays and must remain consistent across re-renders to maintain state integrity.
Where are the plugin components rendered in the DOM hierarchy?
Each plugin's transformTableContext method, as implemented in the source hooks, receives the table children and returns a React fragment wrapping the original content with additional UI elements. For example, the pagination plugin injects controls above or below the table based on the position config, while selection and expansion inject columns and toggles within the table structure itself.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →