5.8 KiB
AdvancedTable
Server-paginated data table with a column-visibility ("View") menu. Built on Mantine Table. Portable — two files, no app-specific imports.
Files
AdvancedTable.tsx— the component.useServerTable.ts— small hook for page-index + search-query state (optional, but pairs with it).
To use in another project, copy both files as-is into that project and export them from your UI barrel (or import by relative path).
Dependencies
@mantine/core (tested on ^8)
@tabler/icons-react (IconRefresh, IconEye, IconInbox)
react-i18next (useTranslation)
react (>=17, uses hooks)
If the target project doesn't use react-i18next, replace the t(key, fallback) calls with plain strings — the component only reads the fallback text, translation is not load-bearing.
Install
npm install @mantine/core @tabler/icons-react react-i18next
Mantine must already be set up with MantineProvider in the app root — this component does not wrap one.
Copy the source
Copy AdvancedTable.tsx and useServerTable.ts into the new project (e.g. src/components/table/). No modifications needed unless you're renaming the i18n keys.
API
AdvancedColumn<T>
| Field | Type | Required | Notes |
|---|---|---|---|
header |
ReactNode |
yes | Column heading, also used as the label in the View menu. |
accessorKey |
string |
no | Dot-path into the row (e.g. "expectation.name"), used when cell is omitted. |
cell |
(ctx: { row: { original: T }; value: unknown }) => ReactNode |
no | Custom cell renderer. Takes priority over accessorKey. |
size |
number |
no | Column width in px. |
align |
'left' | 'center' | 'right' |
no | Text alignment for header + cells. |
enabled |
boolean |
no | Whether the column starts visible. Default true. Toggled at runtime via the View menu. |
AdvancedTable<T> props
| Prop | Type | Required | Notes |
|---|---|---|---|
columns |
AdvancedColumn<T>[] |
yes | |
data |
T[] |
yes | Rows for the current page only — not the full dataset. |
tableName |
string |
yes | Shown as the table title. |
itemCount |
number |
yes | Total row count on the server. Drives pagination and the count badge — not data.length. |
pageIndex |
number |
yes | 0-based current page. |
onPageChange |
(pageIndex: number) => void |
yes | |
pageSize |
number |
no | Default 10. Pagination only renders when itemCount > pageSize. |
onPageSizeChange |
(pageSize: number) => void |
no | Shows a page-size <Select> (10/20/30/40/50 by default) next to the pagination when given. |
pageSizeOptions |
number[] |
no | Options for the page-size select. Default [10, 20, 30, 40, 50]. |
refresh |
() => void |
no | Shows a Refresh button when provided. |
onSearchChange |
(q: string) => void |
no | Reserved for a search box; not currently rendered by the component itself (wire your own input and call this, or drive useServerTable's setQ). |
isLoading |
boolean |
no | Shows a loader row; also spins the Refresh button. |
emptyText |
string |
no | Message when data is empty. |
Rows should have an id: string \| number field — used as the React key (falls back to row index if absent).
useServerTable(opts?)
const { pageIndex, setPageIndex, q, setQ, pageSize, setPageSize, skip, take } = useServerTable({ pageSize: 10 });
Centralizes page-index + search-query state for a server-paginated list. setQ and setPageSize both reset pageIndex back to 0. skip/take are ready to drop into an offset-based API call. Wire setPageSize into AdvancedTable's onPageSizeChange to expose the page-size select.
Behavior notes
- Column visibility is local UI state (
useState), re-initialized from each column'senabledon mount — it does not persist across reloads or sync back to the caller. - At least one column always stays visible; the View menu disables unchecking the last one.
- The View menu closes only via outside click (
closeOnItemClick={false}), so multiple columns can be toggled per open.
Usage
Basic
import { AdvancedTable, type AdvancedColumn } from './table/AdvancedTable';
interface User {
id: string;
name: string;
email: string;
}
const columns: AdvancedColumn<User>[] = [
{ header: 'Name', accessorKey: 'name' },
{ header: 'Email', accessorKey: 'email', enabled: true },
];
<AdvancedTable
columns={columns}
data={users}
tableName="Users"
itemCount={users.length}
pageIndex={0}
onPageChange={() => {}}
/>
Server-paginated, with refresh and a custom action column
import { AdvancedTable, useServerTable, type AdvancedColumn } from './table/AdvancedTable';
function UsersTable() {
const { pageIndex, setPageIndex, skip, take } = useServerTable({ pageSize: 10 });
const { data, isFetching, refetch } = useGetUsersQuery({ skip, take });
const users = data?.items ?? [];
const totalCount = data?.total ?? 0;
const columns: AdvancedColumn<User>[] = [
{ header: 'Name', accessorKey: 'name' },
{ header: 'Email', accessorKey: 'email' },
{
header: 'Status',
align: 'center',
cell: ({ row }) => (row.original.active ? 'Active' : 'Inactive'),
},
{
header: '',
size: 80,
cell: ({ row }) => (
<ActionIcon onClick={() => onEdit(row.original)}>
<IconEdit size={14} />
</ActionIcon>
),
},
];
return (
<AdvancedTable
columns={columns}
data={users}
tableName="Users"
itemCount={totalCount}
pageIndex={pageIndex}
onPageChange={setPageIndex}
pageSize={10}
refresh={refetch}
isLoading={isFetching}
emptyText="No users found"
/>
);
}
Real reference implementation: apps/backoffice/src/app/features/configuration/pages/ConfigurationPage.tsx (ProfessionTab).