munza-x-data-grid
v3.1.4
Published
`Grid` is a reusable data-table component built on top of `@tanstack/react-table`, with built-in support for sorting, filtering, pagination, column pinning, row pinning, column resizing, and more.
Readme
Grid Component — Usage Guide
Grid is a reusable data-table component built on top of @tanstack/react-table, with built-in support for sorting, filtering, pagination, column pinning, row pinning, column resizing, and more.
1. Installation
npm install munza-x-data-grid
# or
yarn add munza-x-data-grid
# or
pnpm add munza-x-data-gridPeer dependencies (must already be installed in your project):
npm install react@^18 || ^19 react-dom@^18 || ^19Importing the stylesheet
The package ships with a CSS file that needs to be imported once at your app's entry point (e.g. main.tsx or App.tsx):
import 'munza-x-data-grid/style.css';Imports
Everything — components, hooks, utilities, and types — is exported from the single package entry point:
import {
Grid,
useGrid,
useGridState,
useQueryArgs,
URLSearch,
type ColumnDef,
} from 'munza-x-data-grid';Note: if you're importing from a local source within the same repo (monorepo / linked package), the path may differ, e.g.
./packageor../package.
2. Basic Usage
import { useMemo } from 'react';
import { Grid, useGridState, type ColumnDef } from 'munza-x-data-grid';
const App = () => {
const columns = useMemo<ColumnDef<MyDataType, unknown>[]>(
() => [
{
id: 'id',
accessorKey: 'id',
header: () => <div>ID</div>,
cell: (info) => info.getValue(),
meta: { filterVariant: 'text' },
enableHiding: false,
},
{
id: 'name',
accessorKey: 'name',
header: () => <div>Name</div>,
cell: (info) => info.getValue(),
meta: { filterVariant: 'text' },
},
],
[],
);
const { state, handlers } = useGridState();
return (
<Grid
payload={{ data: myData, total: 0 }}
columns={columns}
isLoading={false}
isError={false}
manualPagination={true}
state={state}
{...handlers}
/>
);
};
export default App;3. The useGridState Hook
Use this hook to manage all table state (columnFilters, pagination, sorting, rowSelection, globalFilter) and their change handlers in one place.
const { state, handlers, rowSelection } = useGridState();
<Grid
columns={columns}
payload={{ data, total }}
state={state}
{...handlers}
/>;handlers includes:
onColumnFiltersChangeonPaginationChangeonSortingChangesetGlobalFilteronRowSelectionChange
You can also use pieces separately, e.g. tracking selected rows via rowSelection:
console.log(rowSelection);4. Grid Component Props
| Prop | Type | Description |
| ----------------------- | ------------------------------------------ | ----------------------------------------------------------------- |
| columns | ColumnDef<T>[] | Table column definitions (required) |
| payload | { data: T[]; total: number } | Table data and total row count |
| state | Partial<TableState> | State obtained from useGridState |
| onColumnFiltersChange | OnChangeFn<ColumnFiltersState> | Handler for column filter changes |
| onPaginationChange | OnChangeFn<PaginationState> | Handler for pagination changes |
| onSortingChange | OnChangeFn<SortingState> | Handler for sorting changes |
| onRowSelectionChange | OnChangeFn<RowSelectionState> | Handler for row selection changes |
| setGlobalFilter | Dispatch<SetStateAction<string>> | Function to set the global search/filter |
| manualPagination | boolean | Set to true to enable server-side pagination |
| enableRowSelection | boolean | Enables/disables row selection (default true) |
| isLoading | boolean | Shows the loading state |
| isError | boolean | Shows the error state |
| isFetching | boolean | For a refetching indicator |
| refetch | () => void | Function to reload the data |
| renderSubComponent | (props: { row: Row<T> }) => ReactElement | Custom content for expandable rows |
| getRowCanExpand | (row: Row<T>) => boolean | Determines which rows can be expanded |
| height | string | Table height (default '65vh') |
| isToolbar | boolean | Whether to show the toolbar (default true) |
| isPagination | boolean | Whether to show the pagination component (default true) |
| name | string | Unique grid name, used to persist local state (default 'munza') |
| children | ReactNode | Renders extra content inside the grid |
5. Special Column Definition Features
Filter variant
Set meta.filterVariant on each column to determine the filter type. The following variants are supported:
'text''number''tel''url''color''range''select''dateRange''date''datetime-local''month''time''week'
meta: {
filterVariant: 'text', // 'number' | 'tel' | 'url' | 'color' | 'range' | 'select' | 'dateRange' | 'date' | 'datetime-local' | 'month' | 'time' | 'week'
},Example — a numeric range filter:
{
id: 'price',
accessorKey: 'price',
header: () => <div>Price</div>,
cell: (info) => info.getValue(),
meta: { filterVariant: 'range' },
},Example — a date filter:
{
id: 'createdAt',
accessorKey: 'createdAt',
header: () => <div>Created At</div>,
cell: (info) => info.getValue(),
meta: { filterVariant: 'date' },
},To disable filtering on a specific column:
enableColumnFilter: false,Row pinning
Use row.pin() and row.getIsPinned() to pin/unpin rows:
cell: ({ row }) =>
row.getIsPinned() ? (
<button onClick={() => row.pin(false)}>❌</button>
) : (
<>
<button onClick={() => row.pin('top')}>⬆️</button>
<button onClick={() => row.pin('bottom')}>⬇️</button>
</>
),Row number column
{
accessorFn: (_row, index) => index + 1,
id: 'rowNumber',
header: '#',
cell: ({ row, table }) => {
const { pageIndex, pageSize } = table.getState().pagination;
return pageIndex * pageSize + row.index + 1;
},
},Column size limits
size: 52,
maxSize: 52,Non-hideable column
enableHiding: false,6. Building Server-Side Queries
Use the useQueryArgs hook to build query parameters from the table state that can be sent to an API:
import { useQueryArgs } from 'munza-x-data-grid';
const { pagination, queryParams, sort, globalFilter } = useQueryArgs(state);(useQueryArgs is exported from the same main entry point, no separate sub-path needed.)
It returns:
pagination— current page/page sizequeryParams— query parameters built from column filterssort— sort stringglobalFilter— global search text
7. Syncing State to the URL — URLSearch
URLSearch converts the grid's query state (QueryArgs) into a MongoDB-style query string, so you can sync table state to the URL or forward it straight to a REST API that understands Mongo-style filter operators.
Import
import { URLSearch } from 'munza-x-data-grid';QueryArgs shape
interface ColumnFilter {
id: string;
value: unknown;
}
interface SortingItem {
id: string;
desc: boolean;
}
interface Pagination {
pageIndex: number;
pageSize: number;
}
interface QueryArgs {
columnFilters: ColumnFilter[];
globalFilter?: string;
pagination: Pagination;
sorting: SortingItem[];
rowSelection?: Record<string, boolean>;
}Basic usage
const { state } = useGridState();
console.log(URLSearch(state));
// e.g. "?page=1&limit=20&sort=-createdAt&q=john"What it does
URLSearch(queryArgs: QueryArgs): string builds a URLSearchParams string with the following rules:
| State | Resulting query param(s) |
| ---------------------- | ----------------------------------------------------------------------------------------------- |
| pagination.pageIndex | page (1-indexed, so pageIndex + 1) |
| pagination.pageSize | limit |
| sorting | sort — comma-separated column ids, prefixed with - for descending (e.g. sort=-price,name) |
| globalFilter | q — only included if non-empty |
| columnFilters | one or more params per filter, see below |
Column filter handling
Each entry in columnFilters is passed through appendColumnFilter, which converts the filter's id into dot-notation (underscores become dots — useful for nested/populated fields) and then branches on the shape of value:
Range value — a 2-item array (e.g. from a
rangeordateRangefilter):{ id: 'price', value: [10, 100] } // → price[gte]=10&price[lte]=100Empty/
nullbounds are skipped, so[null, 100]only setsprice[lte]=100.Operator object — a plain object whose keys are MongoDB-style operators:
{ id: 'status', value: { in: ['active', 'pending'] } } // → status[in]=active,pendingOnly keys present in the supported operator list are included; unsupported keys and empty/
nullvalues are ignored.Plain scalar value — anything else (string, number, boolean):
{ id: 'name', value: 'john' } // → name=johnEmpty strings and
null/undefinedare skipped entirely.
Supported MongoDB-style operators
eq, ne, gt, gte, lt, lte, in, nin, regex, exists, all, size,
elemMatch, type, mod, not, and, or, nor, text, where,
geoWithin, geoIntersects, near, nearSphere, expr, jsonSchema,
bitsAllClear, bitsAllSet, bitsAnyClear, bitsAnySet, randAny operator outside this list is silently dropped from the query string.
Full example
const queryArgs: QueryArgs = {
pagination: { pageIndex: 0, pageSize: 20 },
sorting: [{ id: 'createdAt', desc: true }],
globalFilter: 'john',
columnFilters: [
{ id: 'price', value: [10, 100] },
{ id: 'status', value: { in: ['active', 'pending'] } },
{ id: 'department_name', value: 'Engineering' },
],
};
URLSearch(queryArgs);
// "?page=1&limit=20&price[gte]=10&price[lte]=100&status[in]=active%2Cpending&department.name=Engineering&sort=-createdAt&q=john"Note:
department_namebecomesdepartment.name— underscores in a columnidare converted to dots, which is handy for filtering on nested/populated document fields in MongoDB-backed APIs.
8. Customizing via useGrid (inside <Grid> children)
Grid exposes its internal context — the live @tanstack/react-table table instance, density, split-view state, column pinning, scroll pane refs, and more — through the useGrid() hook. This lets you build custom toolbar buttons, headers, footers, or any other UI that needs access to the table without having to lift state yourself.
Important:
useGrid()only works inside<Grid>— i.e. in a component rendered as a child of<Grid>. Calling it in the same component that renders<Grid>(like yourAppcomponent) will throw, because the context provider wrapsGrid's children, notGriditself.
Import
import { Grid, useGrid } from 'munza-x-data-grid';What useGrid() returns
| Property | Type | Description |
| -------------------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| table | Table<T> | The underlying @tanstack/react-table instance — full access to rows, columns, filtering/sorting/pagination APIs, etc. |
| density | DensityState | Current row density (e.g. compact/comfortable) |
| isSplit / setIsSplit | boolean / setter | Split-view state, for side-by-side pinned/unpinned panes |
| columnPinning | ColumnPinningState | Current left/right pinned columns |
| globalFilter | string | The current global search text |
| setGlobalFilter | setter | Update the global search text |
| isLoading / isError / isFetching | boolean | Data-fetch status flags passed down to Grid |
| refetch | () => void | Re-triggers the data fetch, if provided to Grid |
| height | string | The table height passed to Grid |
| renderSubComponent | function | The expanded-row renderer passed to Grid |
| gridWrapperRef | RefObject<HTMLDivElement> | Ref to the grid's outer wrapper element |
| paneRef1–paneRef6 | RefObject<HTMLDivElement> | Refs to the internal scroll panes (pinned-left/main/pinned-right, header/body), used for scroll syncing |
Example — a custom toolbar button using table
import { Grid, useGrid, useGridState } from 'munza-x-data-grid';
const ResetFiltersButton = () => {
const { table } = useGrid();
return (
<button onClick={() => table.resetColumnFilters()}>Clear filters</button>
);
};
const App = () => {
const { state, handlers } = useGridState();
return (
<Grid
payload={{ data, total }}
columns={columns}
state={state}
{...handlers}
>
<ResetFiltersButton />
</Grid>
);
};Example — reading selected rows from table
const SelectionSummary = () => {
const { table } = useGrid();
const selectedCount = table.getSelectedRowModel().rows.length;
return <span>{selectedCount} row(s) selected</span>;
};
<Grid columns={columns} payload={{ data, total }} state={state} {...handlers}>
<SelectionSummary />
</Grid>;Example — toggling density or split view
const ViewControls = () => {
const { density, isSplit, setIsSplit } = useGrid();
return (
<div>
<span>Density: {density}</span>
<button onClick={() => setIsSplit((prev) => !prev)}>
{isSplit ? 'Unsplit view' : 'Split view'}
</button>
</div>
);
};
<Grid columns={columns} payload={{ data, total }} state={state} {...handlers}>
<ViewControls />
</Grid>;9. Theming
Grid is styled with CSS variables, so you can restyle it (including light/dark mode) without touching component code. Define these variables in your global stylesheet — the :root block sets the light theme, and the .dark block overrides it for dark mode (e.g. toggled via a dark class on <html> or <body>).
:root {
--background: oklch(100% 0 0);
--foreground: oklch(14.5% 0 0);
--primary: oklch(20.5% 0 0);
--primary-foreground: oklch(98.5% 0 0);
--secondary: oklch(97% 0 0);
--secondary-foreground: oklch(20.5% 0 0);
--muted: oklch(97% 0 0);
--muted-foreground: oklch(55.6% 0 0);
--accent: oklch(97% 0 0);
--accent-foreground: oklch(20.5% 0 0);
--destructive: oklch(57.7% 0.245 27.325);
--border: oklch(92.2% 0 0);
--input: oklch(92.2% 0 0);
--ring: oklch(70.8% 0 0);
--radius: 0.625rem;
}
.dark {
--background: oklch(14.5% 0 0);
--foreground: oklch(98.5% 0 0);
--primary: oklch(92.2% 0 0);
--primary-foreground: oklch(20.5% 0 0);
--secondary: oklch(26.9% 0 0);
--secondary-foreground: oklch(98.5% 0 0);
--muted: oklch(26.9% 0 0);
--muted-foreground: oklch(70.8% 0 0);
--accent: oklch(26.9% 0 0);
--accent-foreground: oklch(98.5% 0 0);
--destructive: oklch(70.4% 0.191 22.216);
--border: oklch(100% 0 0/0.1);
--input: oklch(100% 0 0/0.15);
--ring: oklch(55.6% 0 0);
}Where to put it
Add this CSS after the package stylesheet import, so your custom values override the defaults:
import 'munza-x-data-grid/style.css';
import './theme.css'; // your theme variables aboveVariable reference
| Variable | Used for |
| ------------------------ | ------------------------------------------------------------ |
| --background | Base grid/page background |
| --foreground | Default text color |
| --primary | Primary buttons/actions, active states |
| --primary-foreground | Text/icon color on top of --primary |
| --secondary | Secondary buttons, toolbar surfaces |
| --secondary-foreground | Text/icon color on top of --secondary |
| --muted | Subtle backgrounds (e.g. striped rows, disabled states) |
| --muted-foreground | Secondary/lower-emphasis text |
| --accent | Hover/highlight backgrounds |
| --accent-foreground | Text/icon color on top of --accent |
| --destructive | Error states, destructive actions (e.g. delete) |
| --border | Table/cell/input borders |
| --input | Input field borders/backgrounds |
| --ring | Focus ring color |
| --radius | Base border radius, used to derive component corner rounding |
Toggling dark mode
Add/remove the dark class on a parent element (commonly <html>):
document.documentElement.classList.toggle('dark');10. Example: Full Flow
const { state, handlers } = useGridState();
const queryArgs = useQueryArgs(state);
// Call your API with queryArgs, then pass the result as payload
<Grid
payload={{ data: apiResponse.data, total: apiResponse.total }}
columns={columns}
manualPagination
isLoading={isFetching}
isError={isError}
refetch={refetch}
state={state}
{...handlers}
/>;