Skip to Content

Overview

The Akitable component, is designed to manage and display complex datasets in React applications efficiently. It integrates with Ant Design for robust UI elements and extends functionality with custom theming, pagination, and row selection capabilities.

Key Features

  • Dynamic Data Rendering: Easily render data arrays with customizable columns.
  • Integrated Pagination: Managed through the pagination prop for smooth navigation.
  • Flexible Row Selection: Pass rowSelection directly to enable checkbox/radio selection independently of actions.
  • Customizable Styling: Adapt the table to different UI themes, with options for background removal and scroll behavior.
  • Extensible Design: Works seamlessly with header, footer, and actions props.

Example Usage

import type { AkitableAction, AkitableColumn, AkitablePaginatedData, AkitablePaginationProps, SorterChangeEvent } from '@akinon/akitable'; import { Akitable } from '@akinon/akitable'; const results = [ { key: '1', name: 'Mike', age: 32 }, { key: '2', name: 'John', age: 42 } ]; const MyComponent = () => { const data: AkitablePaginatedData = { count: results.length, results }; const columns: AkitableColumn[] = [ { title: 'Name', dataIndex: 'name', key: 'name' }, { title: 'Age', dataIndex: 'age', key: 'age' } ]; const actions: AkitableAction[] = [ { label: 'Delete', onSelect: (selectedRowKeys: React.Key[]) => { console.log('selectedRowKeys: ', selectedRowKeys); } } // Additional actions... ]; const pagination: AkitablePaginationProps = { page: 1, size: 20 }; const handleSorterChange: SorterChangeEvent = (sorter) => { console.log('Sorter changed: ', sorter); }; return ( <Akitable isLoading={false} data={data} columns={columns} actions={actions} pagination={pagination} rowKey="key" onChangeSorter={handleSorterChange} // Additional Akitable props... /> ); }; export default MyComponent;

Akitable Props

Akitable props provide various options for configuring the table component:

PropertyDescriptionType
isLoadingIndicates if the data is currently loading.boolean
dataArray of data objects to display.AkitableData[] | AkitablePaginatedData
columnsConfiguration for the table columns.AkitableColumn[]
actionsArray of actions that can be performed on selected rows.AkitableAction[]
headerManages the display of the table’s header.AkitableHeaderProps | undefined
footerManages the display of the table’s footer.AkitableFooterProps | undefined
emptyStateCustom empty state configuration for the table.AkitableEmptyStateProps | undefined
rowKeyRow’s unique key.string
paginationPagination settings to control the page navigation. Optionally takes pageSizeOptions to replace the “items per page” list.AkitablePaginationProps | undefined
onRowClickConfiguration for row click features.RowClickEvent | undefined
onRowEditConfiguration for row edit features.RowEditCallback | undefined
actionColumnAction column appended as the last column, replacing the default edit column.AkitableActionColumn | undefined
onChangeSorterCalled when table sorting is changed.SorterChangeEvent | undefined
rowSelectionRow selection configuration. Enables checkbox/radio selection regardless of whether actions are defined. When actions are also present, selectedRowKeys and onChange are managed internally; all other rowSelection fields are merged.AkitableRowSelection | undefined
onPaginationChangedCalled when the page number or page size is changed.PaginationChangeEvent | undefined
paginationShowTotalCustom render function for the pagination total label (e.g. “1-20 of 100 items”).(total: number, range: [number, number]) => ReactNode | undefined

Types

interface AkitableData { [key: string]: any; } interface AkitablePaginatedData { count: number; next?: string | null; previous?: string | null; results: AkitableData[]; }
import type { TableColumnType } from 'antd'; interface AkitableColumn extends TableColumnType<AkitableData> { copyable?: boolean; editable?: boolean; }

You can learn about TableColumnType type here .

interface AkitableAction { label: string; onSelect: (selectedRowKeys: React.Key[]) => void; }
interface AkitableActionColumnRenderParams { record: AkitableData; isEditing: boolean; isEditLoading: boolean; hasEditFormError: boolean; handleStartEdit: (e?: React.MouseEvent) => void; handleSaveEditingItem: () => Promise<void>; handleCancelEditingItem: () => void; } interface AkitableActionColumn { title?: ReactNode; width?: number | string; fixed?: 'left' | 'right' | boolean; className?: string; align?: 'left' | 'right' | 'center'; render?: (params: AkitableActionColumnRenderParams) => ReactNode; items?: AkitableActionItem[]; onNavigate?: ( href: string, record: AkitableData, event: React.MouseEvent ) => void; }
type AkitableActionItemValue<T> = T | ((record: AkitableData) => T); // Every other `@akinon/ui-button` prop (`icon`, `iconSize`, `type`, `danger`, // `tooltip`, `loading`, `ghost`, `block`, `className`, `data-*`, …) is accepted // and forwarded to the rendered button. `size` is fixed to `small` by the // action column, so it is not accepted here. interface AkitableActionItem extends Omit< IButtonProps, 'children' | 'disabled' | 'hidden' | 'href' | 'onClick' | 'size' > { key?: string; label?: ReactNode; href?: AkitableActionItemValue<string>; onClick?: (record: AkitableData, event: React.MouseEvent) => void; startEdit?: boolean; disabled?: AkitableActionItemValue<boolean>; hidden?: AkitableActionItemValue<boolean>; }
interface AkitableHeaderProps { title?: string; extra?: ReactNode; }
interface AkitableFooterProps { extra?: ReactNode; }
interface AkitableEmptyStateProps { icon?: ReactNode; description?: ReactNode; }
// The default sizes stay as literals for editor suggestions, but any number is // accepted so a custom `pageSizeOptions` list is not limited to them. type AkitablePageSizes = 20 | 50 | 100 | 250 | (number & NonNullable<unknown>); interface AkitablePaginationProps { page: number; size: AkitablePageSizes; pageSizeOptions?: readonly AkitablePageSizes[]; }
Customizing the page size options

The “items per page” dropdown offers [20, 50, 100, 250] by default. Pass pageSizeOptions to replace that list — useful when the backend does not support every size. Omitting it (or passing an empty array) keeps the defaults.

Repeated sizes are dropped from the list, but the given order is preserved — the dropdown presents the sizes exactly as they are passed.

size accepts any number, but it must be a positive integer: a 0, negative or fractional size would leave the pager with a nonsensical page count, so the pagination renders the first offered option instead — and reports that size back through onPaginationChanged on the next interaction.

import { AKITABLE_DEFAULT_PAGE_SIZE_OPTIONS, type AkitablePaginationProps } from '@akinon/akitable'; // Only the sizes the backend supports. const pagination: AkitablePaginationProps = { page: 1, size: 20, pageSizeOptions: [20, 50, 100] }; // Or derive the list from the defaults. const derived: AkitablePaginationProps = { page: 1, size: 20, pageSizeOptions: AKITABLE_DEFAULT_PAGE_SIZE_OPTIONS.filter( size => size !== 250 ) };
type RowClickEvent = ( record: AkitableData, event?: React.MouseEvent<HTMLElement>, rowIndex?: number ) => void;
type RowEditCallback = ( modifiedRecord: AkitableData, payload: AkitableData ) => void | Promise<void>;
type PaginationChangeEvent = (page: number, size: number) => void;
type SortOrder = 'descend' | 'ascend' | null; interface SorterResult { column?: AkitableColumn; order?: SortOrder; field?: string | readonly string[]; columnKey?: string; } type TableOnChangeSorter = SorterResult | SorterResult[]; type SorterChangeEvent = (sorter: TableOnChangeSorter) => void;
enum AkitableRowStatus { PENDING = 'pending', ERROR = 'error' }
type AkitableRowSelectionType = 'checkbox' | 'radio'; type AkitableRowSelectMethod = 'all' | 'none' | 'invert' | 'single' | 'multiple'; interface AkitableRowSelection { align?: 'left' | 'center' | 'right'; checkStrictly?: boolean; columnTitle?: ReactNode | ((originalNode: ReactNode) => ReactNode); columnWidth?: string | number; fixed?: boolean | 'left' | 'right'; getCheckboxProps?: (record: AkitableData) => Record<string, unknown>; getTitleCheckboxProps?: () => Record<string, unknown> & React.AriaAttributes; hideSelectAll?: boolean; preserveSelectedRowKeys?: boolean; renderCell?: (checked: boolean, record: AkitableData, index: number, originNode: ReactNode) => ReactNode; selectedRowKeys?: React.Key[]; selections?: object[] | boolean; type?: AkitableRowSelectionType; onChange?: (selectedRowKeys: React.Key[], selectedRows: AkitableData[], info: { type: AkitableRowSelectMethod }) => void; onSelect?: (record: AkitableData, selected: boolean, selectedRows: AkitableData[], nativeEvent: Event) => void; }

Actions that navigate somewhere should be declared through actionColumn.items rather than a custom render. Akitable renders every item that has an href as a real <a href> element, so the browser’s own gestures work on it:

GestureHandled byResult
Left clickAkitable → onNavigateClient-side navigation, no page reload
Middle clickThe browserOpens in a new tab
Cmd/Ctrl/Shift + clickThe browserOpens in a new tab or window
Right click → open in a new tabThe browserOpens in a new tab

A plain left click is intercepted (preventDefault) and delegated to the item’s own onClick if it has one, otherwise to the column’s onNavigate — which keeps SPA routing intact. When neither is defined the click is left alone and the browser follows the href as a regular link. No target is set on the anchor, because that would make a plain left click open a new tab as well.

Non-navigating actions (delete, toggles, …) use onClick instead of href and render as plain buttons, so they are unaffected.

An item that is disabled or loading renders as a plain button as well, even when it has an href: antd ignores a Button’s click handler in both states, so an anchor left in place would be followed on a middle click while a plain left click did nothing. Clicking an action never triggers the table’s onRowClick, in any of these states.

<Akitable data={data} columns={columns} rowKey="id" actionColumn={{ title: 'Actions', fixed: 'right', onNavigate: (href) => navigate(href), items: [ { key: 'edit', icon: 'edit', tooltip: 'Edit', type: 'link', href: (record) => `/products/${record.id}/edit` }, { key: 'delete', icon: 'bin', tooltip: 'Delete', type: 'text', danger: true, onClick: (record) => remove(record.id) } ] }} />

href, disabled and hidden accept either a static value or a function of the row, so each row can resolve its own target and state.

Since the href becomes a real anchor attribute, it must be a URL the browser can resolve on its own — for example /products/1, or #/products/1 when a hash router is used.

Because an href is typically derived from record data, it is validated rather than trusted: a real anchor is followed natively on middle click and Cmd/Ctrl + click, where no click handler runs to intercept it. Only relative URLs (/products/1, products/1, #/products/1, ?page=2) and the http, https, mailto and tel schemes are accepted. Everything else is rejected — javascript: above all, but also a protocol-relative URL (//evil.com, plus the \\evil.com / /\evil.com shapes browsers parse the same way), an empty or whitespace-only value, and an internal routing key shaped like catalog:detail, which parses as the scheme catalog:.

A rejected value is dropped silently: nothing is logged and the item renders as a plain button. Check the values your href builder can produce rather than waiting for a console message — record => record.detailUrl ?? '' is an easy slip that turns a link into a button.

Precedence and Backwards Compatibility

render takes precedence over items when both are provided, and a warning is logged. This keeps every existing actionColumn.render usage working unchanged.

Note that anything returned from render is opaque to Akitable, so a <button onClick> rendered there will not gain new-tab support. Either migrate the navigating actions to items, or render your own real <a href> elements inside render.

To put the row into edit mode from an item, set startEdit: true instead of an onClick. This requires the table’s onRowEdit to be defined, since that is what saves the row. While a row is being edited, Akitable renders its built-in save and cancel actions in place of the items.