@hazemazzam/paginated-data-table
v1.2.0
Published
Compound components for building paginated data table layouts
Maintainers
Readme
@hazemazzam/paginated-data-table
Compound components for paginated data table layouts with React + TanStack Table.
This package provides the state and UX shell for paginated tables:
- loading overlay (first load + refetch)
- error state rendering
- table area layout
- optional batch-actions slot
- optional column visibility persistence via a pluggable storage adapter (since
1.1.0)
It is intentionally UI-agnostic through compound components, so you can plug in your own design system.
Installation
npm i @hazemazzam/paginated-data-tablePeer Dependencies
reactreact-dom@tanstack/react-table
Exports
PaginatedDataTable(compound component API)usePaginatedDataTableContextTableControlsTableActionsTablePaginationPaginatedListQueryFlags(type)PaginatedTablePaginationState(type)TablePaginationProps(type)Visibility(type) — column visibility storage adapter
Compound Component Usage
Use this API when you want full control over layout order and composition.
import {
PaginatedDataTable,
TableControls,
TableActions,
TablePagination,
} from "@hazemazzam/paginated-data-table";
<PaginatedDataTable.Root table={table} query={query} pagination={pagination}>
<PaginatedDataTable.Container className="rounded-lg border p-2">
<PaginatedDataTable.LoadingOverlay />
<PaginatedDataTable.ErrorState>
{(error) => <div>Custom error: {String(error)}</div>}
</PaginatedDataTable.ErrorState>
<PaginatedDataTable.Content>
<PaginatedDataTable.Controls>
{({ pagination }) => (
<TableControls>
<div>Filters area</div>
<TableActions>
<TablePagination
page={pagination.page}
pages={pagination.pages}
setPage={pagination.setPage}
/>
</TableActions>
</TableControls>
)}
</PaginatedDataTable.Controls>
<PaginatedDataTable.Table>
{(table) => <MyDataTable table={table} />}
</PaginatedDataTable.Table>
</PaginatedDataTable.Content>
</PaginatedDataTable.Container>
<PaginatedDataTable.BatchActions>
<MyBatchActions />
</PaginatedDataTable.BatchActions>
</PaginatedDataTable.Root>;You can optionally guard against stuck requests:
<PaginatedDataTable.Root
table={table}
query={query}
pagination={pagination}
loadingTimeoutMs={15000}
loadingTimeoutMessage="Request timed out. Please retry."
>
{/* ... */}
</PaginatedDataTable.Root>Column Visibility Persistence (since 1.1.0)
<PaginatedDataTable.Root> accepts an optional visibility adapter prop. When provided, the table's column visibility is hydrated from storage on mount and saved on every change. The package is storage-agnostic — you supply the adapter, so you can persist to sessionStorage, localStorage, IndexedDB, a server, or anywhere else.
Adapter shape
type Visibility = {
load: () => VisibilityState | undefined;
save: (state: VisibilityState) => void;
};Behavior
- On mount: if
visibility.load()returns a state, it's applied viatable.setColumnVisibility(...). - On every visibility change:
visibility.save(currentState)is called. A JSON-snapshot dedupe avoids storage thrash on unrelated re-renders. - When
visibilityis omitted: behavior is identical to1.0.x— no hydration, no save. Fully backwards compatible.
Example: sessionStorage adapter (vanilla)
import {
PaginatedDataTable,
type Visibility,
} from "@hazemazzam/paginated-data-table";
const STORAGE_KEY = "my-table-visibility";
const visibility: Visibility = {
load: () => {
const raw = sessionStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : undefined;
},
save: (state) => {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
},
};
<PaginatedDataTable.Root
table={table}
query={query}
pagination={pagination}
visibility={visibility}
>
{/* ... */}
</PaginatedDataTable.Root>;Example: zustand-backed adapter, keyed by table name
For multiple tables in the same app, key the persisted state by an opaque table name so user preferences don't bleed across tables:
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import {
PaginatedDataTable,
type Visibility,
} from "@hazemazzam/paginated-data-table";
import type { VisibilityState } from "@tanstack/react-table";
// Single store for every table in your app
const useTableVisibilityStore = create<{
byTable: Record<string, VisibilityState>;
setVisibility: (tableName: string, state: VisibilityState) => void;
}>()(
persist(
(set) => ({
byTable: {},
setVisibility: (tableName, state) =>
set((s) => ({ byTable: { ...s.byTable, [tableName]: state } })),
}),
{
name: "table-visibility",
storage: createJSONStorage(() => sessionStorage),
},
),
);
// Adapter hook — call inside your table wrapper
function useTableVisibility(tableName: string | undefined): Visibility | undefined {
const stored = useTableVisibilityStore((s) =>
tableName ? s.byTable[tableName] : undefined,
);
const setVisibility = useTableVisibilityStore((s) => s.setVisibility);
if (!tableName) return undefined;
return {
load: () => stored,
save: (state) => setVisibility(tableName, state),
};
}
// Usage
function ProductsTable() {
const visibility = useTableVisibility("products");
// ...
return (
<PaginatedDataTable.Root
table={table}
query={query}
pagination={pagination}
visibility={visibility}
>
{/* ... */}
</PaginatedDataTable.Root>
);
}Pass a different tableName per table to keep their preferences independent. Pass undefined to opt out of persistence entirely.
Props
query (PaginatedListQueryFlags)
isPending: booleanisFetching: booleanisError: booleanhasData: booleanerror?: unknown
pagination (PaginatedTablePaginationState)
page: numberpages: numberlimit: numbersetPage(page: number): voidsetLimit(limit: number): void
Root optional props
loadingTimeoutMs?: number(default15000)loadingTimeoutMessage?: string(default"Request is taking too long. Please try again.")visibility?: Visibility— column visibility storage adapter (since1.1.0)
Extra Components
TableControls
A simple flex container (justify-between) for the table toolbar row.
TableActions
A right-aligned flex container (justify-end) meant for page-size and pagination controls.
TablePagination
Simple previous/next pagination component with current page display.
import { TablePagination } from "@hazemazzam/paginated-data-table";
<TablePagination page={page} pages={pages} setPage={setPage} />;Behavior Notes
- Shows loading overlay when:
- first load (
isPending) - fetching before any successful data (
isFetching && !hasData)
- first load (
- Shows refetch overlay when fetching while data exists (
isFetching && hasData) - Shows error section only when
isError && !hasData - If initial loading exceeds
loadingTimeoutMs, loading overlay is replaced by error state - Renders table content otherwise
- When
visibilityis provided, column visibility hydrates once on mount and persists on every change
Changelog
1.1.0
- Added: optional
visibilityprop on<PaginatedDataTable.Root>for column visibility persistence via a pluggable storage adapter. See Column Visibility Persistence. - Added:
Visibilitytype export. - Backwards compatible — omitting
visibilitykeeps1.0.xbehavior. No new runtime dependencies.
1.0.x
- Initial compound component API with loading/error/refetch states, batch-actions slot, and pagination helpers.
Versioning
Semver:
- patch: docs and fixes
- minor: backward-compatible API additions
- major: breaking API changes
