@niveshmintra/react-datatable
v0.1.2
Published
Headless, dependency-free React data table. Bring your own data source and styles.
Maintainers
Readme
@niveshmintra/react-datatable
Headless, dependency-free React data table. Bring your own data source and styles.
Server-side or client-side sorting, pagination, search, and row selection — with
zero runtime dependencies. The core (useDataTable) renders nothing; you own the
markup. A styled <DataTable> component and an opt-in theme ship on top.
- Headless —
useDataTablereturns a model; render whatever you want. - Or batteries-included —
<DataTable>ships accessible semantic markup. - Client or server mode — in-memory data, or an async data source.
- Fully typed — generic over your row type; ships
.d.ts(ESM + CJS).
Install
npm install @niveshmintra/react-datatablereact and react-dom (>=18) are peer dependencies.
Quick start (client mode)
import { useDataTable, type ColumnDef } from '@niveshmintra/react-datatable';
interface Person { id: number; name: string; age: number; }
const columns: ColumnDef<Person>[] = [
{ id: 'name', header: 'Name', accessorKey: 'name' },
{ id: 'age', header: 'Age', accessorKey: 'age' },
];
function People({ data }: { data: Person[] }) {
const table = useDataTable({ columns, data, pageSize: 10 });
return (
<table>
<thead>
<tr>
{table.headers.map((h) => (
<th
key={h.column.id}
onClick={() => h.toggleSort()}
aria-sort={
h.sortDirection === 'asc'
? 'ascending'
: h.sortDirection === 'desc'
? 'descending'
: 'none'
}
>
{String(h.column.header)}
</th>
))}
</tr>
</thead>
<tbody>
{table.rows.map((row) => (
<tr key={row.id}>
{row.cells.map((cell) => (
<td key={cell.column.id}>{cell.render()}</td>
))}
</tr>
))}
</tbody>
</table>
);
}Or skip the markup entirely and use the styled component.
Columns
A ColumnDef<TData> describes one column. Resolve the value with either
accessorKey (a flat key on the row) or accessorFn (derive it), and
optionally override rendering with cell.
const columns: ColumnDef<Person>[] = [
// Flat key.
{ id: 'name', header: 'Name', accessorKey: 'name' },
// Derived value.
{ id: 'full', header: 'Full name', accessorFn: (p) => `${p.first} ${p.last}` },
// Custom cell renderer (gets row, value, column, rowIndex).
{
id: 'age',
header: 'Age',
accessorKey: 'age',
cell: ({ value }) => <strong>{value as number}</strong>,
},
// Action column: no accessor, sorting off, fixed width, pinned right.
{
id: 'actions',
header: '',
enableSorting: false,
width: 80,
pinned: 'right',
cell: ({ row }) => <button onClick={() => edit(row)}>Edit</button>,
},
];| Field | Type | Notes |
|-------|------|-------|
| id | string | Required. Stable unique id (sort/selection key). |
| header | ReactNode \| () => ReactNode | Header content. |
| accessorKey | keyof TData & string | Read value from this key. |
| accessorFn | (row) => unknown | Derive value (overrides accessorKey). |
| cell | (ctx) => ReactNode | Custom renderer; falls back to raw value. |
| enableSorting | boolean | Per-column sort toggle (default: table-level). |
| width | number \| string | Fixed width; omit to flex-fill. |
| pinned | 'left' \| 'right' | Sticky column side. |
| meta | Record<string, unknown> | Untyped escape hatch for adapter data. |
Server mode
Pass dataSource instead of data. It's a single async function
(state) => { rows, total } called whenever sort / page / search changes.
Implement it with fetch, axios, Inertia, GraphQL — anything.
import { useDataTable, createRestDataSource } from '@niveshmintra/react-datatable';
const dataSource = createRestDataSource<Person>({
url: '/api/people',
method: 'POST',
});
function People() {
const table = useDataTable({ columns, dataSource, pageSize: 20 });
// table.isLoading, table.error, table.total ...
}⚠️ Memoize the data source. A
dataSourcecreated inline in render is a new reference every render → infinite refetch loop. Define it at module scope (as above) or wrap it inuseMemo:const dataSource = useMemo(() => createRestDataSource<Person>({ url }), [url]);
createRestDataSource
A native-fetch helper (no axios). Both the request and response shapes are
overridable to fit any backend.
Default request (method defaults to POST, sent as JSON body; GET sends
query params):
{ "page": 1, "per_page": 20, "sort": [{ "id": "name", "dir": "asc" }], "search": "ann" }Default response parsing reads rows from data or rows, and total from
pagination.total or total (falling back to rows.length):
{ "data": [/* rows */], "pagination": { "total": 137 } }Override either side:
createRestDataSource<Person>({
url: '/api/people',
headers: { Authorization: `Bearer ${token}` },
serializeQuery: (s) => ({ offset: s.pagination.pageIndex * s.pagination.pageSize, q: s.search }),
parseResponse: (json) => ({ rows: json.items, total: json.count }),
});For auth/interceptors beyond headers, write your own DataSource function — the
package knows nothing about your backend.
Styled component
import { DataTable } from '@niveshmintra/react-datatable';
import '@niveshmintra/react-datatable/styles.css'; // opt-in theme
<DataTable columns={columns} data={data} enableRowSelection onRowClick={open} />Renders semantic <table> markup with stable rdt-* class names. Accessibility
is built in: sortable headers are <button>s with aria-sort; row selection
adds a select-all-on-page header checkbox. The theme is opt-in — every slot takes
a class via classNames, and the component never depends on the CSS.
Sorting
Single-column by default — header click cycles asc → desc → cleared. Set
enableMultiSort to stack columns with shift-click.
API reference
useDataTable(options) — options
| Option | Type | Default | Notes |
|--------|------|---------|-------|
| columns | ColumnDef<TData>[] | — | Required. |
| data | TData[] | — | Client mode. Mutually exclusive with dataSource. |
| dataSource | (state) => Promise<{ rows, total }> | — | Server mode. Memoize it. |
| pageSize | number | 10 | Initial page size. |
| enableSorting | boolean | true | Global toggle; per-column overrides. |
| enableMultiSort | boolean | false | Shift-click to stack sorts. |
| enableRowSelection | boolean | false | |
| getRowId | (row, index) => string | array index | Stable row id. |
| searchDebounceMs | number | 300 | Debounce before search recomputes/refetches. |
Return value — DataTableInstance
| Member | Type | Notes |
|--------|------|-------|
| headers | HeaderCell[] | column, sortDirection, canSort, toggleSort(additive?). |
| rows | TableRow[] | id, original, index, selected, toggleSelected(), cells[]. |
| pagination | { pageIndex, pageSize } | |
| pageCount / total | number | |
| setPageIndex / setPageSize | (n) => void | |
| nextPage / previousPage | () => void | |
| canNextPage / canPreviousPage | boolean | |
| search / setSearch | string / (v) => void | Debounced. |
| sorting | ColumnSort[] | Active sort state. |
| selectedRowIds | string[] | |
| clearSelection | () => void | |
| isLoading / error | boolean / Error \| null | Server mode. |
| refresh | () => void | Force refetch (server) / recompute (client). |
<DataTable> — extra props
Accepts every useDataTable option, plus:
| Prop | Type | Default | Notes |
|------|------|---------|-------|
| className | string | — | Applied to the root. |
| classNames | per-slot object | — | Override root, toolbar, search, tableWrapper, table, th, td, tr, pagination. |
| showToolbar | boolean | true | Search toolbar. |
| showPagination | boolean | true | Pagination footer. |
| emptyMessage | ReactNode | 'No rows' | |
| loadingMessage | ReactNode | 'Loading…' | Shown during server fetch. |
| searchPlaceholder | string | 'Search…' | |
| pageSizeOptions | number[] | [10, 20, 50, 100] | |
| onRowClick | (row: TData) => void | — | Adds a pointer cursor to rows. |
Changelog
See CHANGELOG.md.
