@da-core/data-table
v1.2.0
Published
Small typed React DataTable built on top of TanStack Table.
Downloads
546
Maintainers
Readme
@da-core/data-table
Typed React DataTable built on top of TanStack Table.
The package is intentionally small and application-agnostic. It does not know about your router, modal system, Bootstrap, shadcn, Next.js, Remix, server fetching, or URL search params. Those concerns should be composed outside the table.
Install
pnpm add @da-core/data-table @tanstack/react-tableimport "@da-core/data-table/styles.css";Runtime entry points
Use explicit entry points in React Server Components, Next.js App Router, Remix, or any server-first setup:
import { parseDataTableState } from "@da-core/data-table/server";
import { DataTable } from "@da-core/data-table/client";Recommended rule:
- import interactive React components and hooks from
@da-core/data-table/client - import pure state and URL helpers from
@da-core/data-table/server - keep the root entry point for compatibility or non-RSC applications
Basic usage
Use defineDataTableColumns<T>() when declaring columns. It keeps cell, header, footer, accessorFn, and callback params contextually typed for the consumer project.
import { DataTable, defineDataTableColumns } from "@da-core/data-table/client";
import "@da-core/data-table/styles.css";
type UserRow = {
id: string;
name: string;
email: string;
status: "active" | "disabled";
};
const columns = defineDataTableColumns<UserRow>()([
{
accessorKey: "name",
header: "Name",
meta: {
filter: {
type: "text",
placeholder: "Filter name…",
},
},
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => row.original.status,
meta: {
filter: {
type: "select",
options: [
{ label: "Active", value: "active" },
{ label: "Disabled", value: "disabled" },
],
},
},
},
]);
export function UsersTable({ users }: Readonly<{ users: readonly UserRow[] }>) {
return <DataTable columns={columns} data={users} enableGlobalFilter getRowId={(row) => row.id} />;
}Inside cell, row.original is typed as UserRow.
Single-column helper
Use defineDataTableColumn<T>() when a column is extracted into a separate file or factory.
import { defineDataTableColumn } from "@da-core/data-table/client";
export const userEmailColumn = defineDataTableColumn<UserRow>()({
accessorKey: "email",
header: "Email",
cell: ({ row }) => <a href={`mailto:${row.original.email}`}>{row.original.email}</a>,
});Then compose it with the array helper:
const columns = defineDataTableColumns<UserRow>()([
{
accessorKey: "name",
header: "Name",
},
userEmailColumn,
]);Why not only DataTableColumnDef<T>[]?
This works for simple cases:
import type { DataTableColumnDef } from "@da-core/data-table/client";
const columns: DataTableColumnDef<UserRow>[] = [];However, TanStack's ColumnDef is a complex union type. In consumer projects, TypeScript can lose contextual typing for destructured callback params:
cell: ({ row }) => row.original.name;When that happens, row may become implicit any. The defineDataTableColumns<T>() helper gives TypeScript a narrower input type at the authoring boundary, while the table still receives TanStack-compatible column definitions internally.
Wrapping the table in an application component
If your app has an AdminDataTable, keep the wrapper generic. Do not collapse row types to unknown, object, or any.
import { DataTable, type DataTableColumnDef, type DataTableProps } from "@da-core/data-table/client";
type AdminDataTableProps<TData> = Readonly<{
rows: readonly TData[];
columns: readonly DataTableColumnDef<TData>[];
getRowId?: DataTableProps<TData>["getRowId"];
}>;
export function AdminDataTable<TData>({ rows, columns, getRowId }: AdminDataTableProps<TData>) {
return <DataTable columns={columns} data={rows} getRowId={getRowId} />;
}Then consumer callbacks stay typed:
<AdminDataTable columns={columns} rows={rows} getRowId={(row) => row.id} />Column meta and filters
Column filters are explicit. A column is filterable only when it has meta.filter or when you override TanStack's enableColumnFilter manually.
const columns = defineDataTableColumns<UserRow>()([
{
accessorKey: "age",
header: "Age",
meta: {
align: "right",
filter: {
type: "number",
placeholder: "Age…",
},
},
},
]);Supported filter types:
textnumberbooleanselect
For select, option values can be string, number, or boolean.
Action columns
The helper is intentionally router/modal agnostic.
import { createActionColumn, defineDataTableColumns } from "@da-core/data-table/client";
const columns = defineDataTableColumns<UserRow>()([
{
accessorKey: "name",
header: "Name",
},
createActionColumn<UserRow>({
id: "remove",
label: (row) => `Remove ${row.name}`,
renderIcon: () => "🗑️",
onAction: (row) => removeUser(row.id),
}),
]);Controlled state
Use controlled state when the table state belongs to a page, route, query layer, URL adapter, or external store.
onStateChange and useDataTableState().setState accept both direct values and functional updaters. This mirrors React/TanStack state APIs and keeps the currentState parameter typed in consumer wrappers.
import { DataTable, useDataTableState } from "@da-core/data-table/client";
const { state, setState } = useDataTableState({
initialState: {
pagination: { pageIndex: 0, pageSize: 20 },
},
});
<DataTable columns={columns} data={rows} state={state} onStateChange={setState} />;setState((currentState) => ({
...currentState,
pagination: {
...currentState.pagination,
pageIndex: 0,
},
}));When you need a typed URL adapter, derive the updater type from the public props:
import { DataTable, useDataTableState } from "@da-core/data-table/client";
import type { DataTableProps } from "@da-core/data-table/client";
import { parseDataTableState, serializeDataTableState } from "@da-core/data-table/server";
type DataTableStateUpdater<TRow> = Parameters<NonNullable<DataTableProps<TRow>["onStateChange"]>>[0];
function handleStateChange(nextStateOrUpdater: DataTableStateUpdater<UserRow>) {
setState((currentState) => {
const nextState =
typeof nextStateOrUpdater === "function" ? nextStateOrUpdater(currentState) : nextStateOrUpdater;
const nextParams = serializeDataTableState(nextState);
// router.replace(...)
return nextState;
});
}URL state helpers
The package provides pure serialization helpers, but no router integration.
import { parseDataTableState, serializeDataTableState } from "@da-core/data-table/server";
const state = parseDataTableState(new URLSearchParams(location.search));
const params = serializeDataTableState(state);That keeps the package usable with Next.js, TanStack Router, React Router, Remix, custom history, or no router at all.
Styling
Default styles are shipped as a plain CSS file:
import "@da-core/data-table/styles.css";For design systems, use unstyled and provide classNames:
<DataTable
columns={columns}
data={rows}
unstyled
classNames={{
table: "my-table",
headerCell: "my-table-header-cell",
cell: "my-table-cell",
}}
/>Boolean values are not auto-rendered as checkboxes. Render them explicitly in the column cell, because true and false can mean different things in different products.
Quality gate
npm run checkThis runs:
npm run typecheck
npm run lint
npm run buildPublishing
npm run build
npm publish --access publicThe package contains:
- ESM and CJS builds
- explicit
/clientand/serverentry points - generated
.d.tstypes - externalized
reactand@tanstack/react-table - separate
./styles.cssexport publishConfig.access = "public"
