# How to Use Astryx Table Components with Pagination, Selection, and Row Expansion

> Learn how to use Astryx Table components with pagination, selection, and row expansion. Discover the plugin architecture and independent hooks for enhanced table functionality.

- Repository: [Meta/astryx](https://github.com/facebook/astryx)
- Tags: how-to-guide
- Published: 2026-08-06

---

**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`](https://github.com/facebook/astryx/blob/main/packages/core/src/Table/Table.tsx) and [`packages/core/src/Table/BaseTable.tsx`](https://github.com/facebook/astryx/blob/main/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:

```tsx
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`](https://github.com/facebook/astryx/blob/main/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.

```tsx
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 sizes
- **`align`**: 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`](https://github.com/facebook/astryx/blob/main/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.

```tsx
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 identifier
- **`selectedKeys`**: Controlled array of selected row keys; the plugin calls `onSelectionChange` whenever 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`](https://github.com/facebook/astryx/blob/main/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.

```tsx
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.

```tsx
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`](https://github.com/facebook/astryx/blob/main/packages/core/src/Table/Table.tsx)
- **Base table implementation**: [`packages/core/src/Table/BaseTable.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Table/BaseTable.tsx)
- **Pagination plugin**: [`packages/core/src/Table/plugins/pagination/useTablePagination.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Table/plugins/pagination/useTablePagination.tsx)
- **Selection plugin**: [`packages/core/src/Table/plugins/selection/useTableSelection.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Table/plugins/selection/useTableSelection.tsx)
- **Row expansion plugin**: [`packages/core/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx)
- **Table context**: [`packages/core/src/Table/TableContext.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/Table/TableContext.ts)
- **Storybook examples**: [`apps/storybook/stories/TablePagination.stories.tsx`](https://github.com/facebook/astryx/blob/main/apps/storybook/stories/TablePagination.stories.tsx), [`TableSelection.stories.tsx`](https://github.com/facebook/astryx/blob/main/TableSelection.stories.tsx), [`TableRowExpansion.stories.tsx`](https://github.com/facebook/astryx/blob/main/TableRowExpansion.stories.tsx)

## Summary

- **Astryx Tables use a plugin system** where features are independent hooks returning `TablePlugin` objects that transform the table context
- The **`plugins` prop** accepts an object mapping feature names (like `pagination`, `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 `selectedKeys` and `expandedKeys` arrays with `getRowKey` for unique identification
- **Plugins compose automatically** through their `transformTableContext` methods, 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`](https://github.com/facebook/astryx/blob/main/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`](https://github.com/facebook/astryx/blob/main/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.