Skip to content

React Integration

The @toolbox-web/grid-react package provides React integration for the <tbw-grid> data grid component.

React versionSupport level
19Tested — used in demos and CI
18Tested
< 18Not supported (minimum peer dependency)
Terminal window
npm install @toolbox-web/grid @toolbox-web/grid-react
yarn add @toolbox-web/grid @toolbox-web/grid-react
pnpm add @toolbox-web/grid @toolbox-web/grid-react
bun add @toolbox-web/grid @toolbox-web/grid-react

Register the grid web component in your application entry point:

// main.tsx or index.tsx
import '@toolbox-web/grid';

The simplest way to use the grid is with the DataGrid component:

import { DataGrid } from '@toolbox-web/grid-react';
interface Employee {
id: number;
name: string;
department: string;
salary: number;
}
function EmployeeGrid() {
const [employees] = useState<Employee[]>([
{ id: 1, name: 'Alice', department: 'Engineering', salary: 95000 },
{ id: 2, name: 'Bob', department: 'Marketing', salary: 75000 },
{ id: 3, name: 'Charlie', department: 'Sales', salary: 85000 },
]);
return (
<DataGrid
rows={employees}
columns={[
{ field: 'id', header: 'ID', width: 70 },
{ field: 'name', header: 'Name', sortable: true },
{ field: 'department', header: 'Department', sortable: true },
{ field: 'salary', header: 'Salary', type: 'number' },
]}
style={{ height: 400 }}
/>
);
}

Features are enabled using declarative props combined with side-effect imports. This gives you clean JSX and tree-shakeable bundles.

  1. Import the feature - A side-effect import registers the feature factory
  2. Use the prop - DataGrid detects the prop and creates the plugin instance
// 1. Import features you need (once per feature)
import '@toolbox-web/grid-react/features/selection';
import '@toolbox-web/grid-react/features/multi-sort';
import '@toolbox-web/grid-react/features/editing';
import '@toolbox-web/grid-react/features/filtering';
import { DataGrid } from '@toolbox-web/grid-react';
function EmployeeGrid() {
return (
<DataGrid
rows={employees}
columns={columns}
// 2. Just use the props - plugins are created automatically!
selection="range" // SelectionPlugin with mode: 'range'
multiSort // MultiSortPlugin
editing="dblclick" // EditingPlugin with editOn: 'dblclick'
filtering // FilteringPlugin with defaults
/>
);
}
  • Tree-shakeable - Only the features you import are bundled
  • Synchronous - No loading states, no HTTP requests, no spinners
  • Type-safe - Full TypeScript support for feature props
  • Clean JSX - No plugins: [new SelectionPlugin({ mode: 'range' })] boilerplate
ImportPropExample
features/selectionselectionselection="range" or selection={{ mode: 'row', checkbox: true }}
features/multi-sortmultiSortmultiSort or multiSort={{ maxSortColumns: 3 }}
features/filteringfilteringfiltering or filtering={{ debounceMs: 200 }}
features/editingeditingediting="dblclick" or editing="click"
features/clipboardclipboardclipboard (requires selection)
features/undo-redoundoRedoundoRedo (requires editing)
features/context-menucontextMenucontextMenu
features/reorder-columnsreorderColumnsreorderColumns (column drag-to-reorder)
features/visibilityvisibilityvisibility (column visibility panel)
features/pinned-columnspinnedColumnspinnedColumns
features/grouping-columnsgroupingColumnsgroupingColumns
features/grouping-rowsgroupingRowsgroupingRows={{ groupOn: (row) => row.department }}
features/treetreetree={{ childrenField: 'children' }}
features/master-detailmasterDetailmasterDetail (use with <GridDetailPanel>)
features/responsiveresponsiveresponsive (card layout on mobile)
features/exportexportexport
features/printprintprint
features/pinned-rowspinnedRowspinnedRows or pinnedRows={{ position: 'bottom' }}
features/column-virtualizationcolumnVirtualizationcolumnVirtualization
features/reorder-rowsreorderRowsreorderRows
features/pivotpivotpivot={{ rowFields: ['category'], valueField: 'sales' }}
features/server-sideserverSideserverSide={{ pageSize: 50 }}
features/row-drag-droprowDragDroprowDragDrop (cross-grid / external row drag-and-drop)
features/tooltiptooltiptooltip (cell / header tooltips)

These props control core grid behaviour and don’t require feature imports. See DataGridProps for the full prop surface and AllFeatureProps for every typed feature prop.

PropTypeDefaultDescription
rowsT[][]Row data — omit when using serverSide
columnsColumnConfig[]Column definitions (alternative to gridConfig.columns)
gridConfigGridConfigFull configuration object — always wrap in useMemo
refRef<DataGridRef>Imperative handle (also returned by useGrid())
loadingbooleanfalseShow the grid’s loading overlay
fitModeFitMode'auto'Column-fit strategy
customStylesstringCSS injected into the grid (renderer/editor styling)
ssrbooleanfalseDeprecated — no-op in practice; will be removed in a future major release
sortablebooleantrueGrid-wide sorting toggle. Set false to disable all sorting.
filterablebooleantrueGrid-wide filtering toggle. Requires FilteringPlugin.
selectablebooleantrueGrid-wide selection toggle. Requires SelectionPlugin.
// Disable sorting and selection at runtime
<DataGrid
sortable={false}
selectable={false}
selection="range" // Plugin loaded but disabled via selectable={false}
/>

For prototyping or when bundle size isn’t critical:

// Import all features at once
import '@toolbox-web/grid-react/features';
// Now all feature props work
<DataGrid selection="range" multiSort filtering editing="dblclick" clipboard />

Use GridToolPanel to declare a sidebar panel inline. The component renders a <tbw-grid-tool-panel> element in the grid’s light DOM — the shell picks it up automatically, so no gridConfig.shell.toolPanels registration is needed. The children prop is a render function that receives a ToolPanelContext ({ grid }).

import { DataGrid, GridToolPanel, type ToolPanelContext } from '@toolbox-web/grid-react';
function EmployeeGrid() {
return (
<DataGrid rows={employees} columns={columns}>
<GridToolPanel
id="filters"
title="Quick Filters"
icon="🔍"
order={10}
>
{({ grid }: ToolPanelContext) => (
<div style={{ padding: 16 }}>
<h3>Quick Filters</h3>
<label>
<input type="checkbox" /> Active Only
</label>
</div>
)}
</GridToolPanel>
</DataGrid>
);
}

Props: id (required), title (required), icon, tooltip, order (default 100, lower = first).

The useGrid() hook provides programmatic access to the grid — see UseGridReturn for the full return shape. It exposes core grid operations only; feature-specific operations (export, selection, filtering, etc.) live on the dedicated feature hooks (useGridSelection, useGridExport, …) listed below.

import { DataGrid, useGrid } from '@toolbox-web/grid-react';
function EmployeeGrid() {
const {
ref, // Pass to DataGrid
element, // Direct grid element access
isReady, // True once the grid has booted
config, // Current effective GridConfig (snapshot)
getConfig, // async: re-read effective config
forceLayout, // async: force a layout pass
toggleGroup, // async: toggle a group row by key
getPlugin, // Get plugin instance by class
getPluginByName, // Get plugin instance by registered name
registerStyles, // Inject scoped CSS
unregisterStyles,
getVisibleColumns, // Filtered list of non-hidden columns
} = useGrid<Employee>();
return (
<div>
<button onClick={() => forceLayout()}>Force Layout</button>
<button disabled={!isReady} onClick={async () => console.log(await getConfig())}>
Log Config
</button>
<DataGrid ref={ref} rows={employees} columns={columns} />
</div>
);
}

For selection, export, filtering, undo-redo, or print operations, use the feature hooks — they handle the per-plugin wiring:

import '@toolbox-web/grid-react/features/selection';
import '@toolbox-web/grid-react/features/export';
import { useGridSelection } from '@toolbox-web/grid-react/features/selection';
import { useGridExport } from '@toolbox-web/grid-react/features/export';
function Toolbar() {
const { selectAll, clearSelection, getSelectedRows } = useGridSelection<Employee>();
const { exportToCsv, isExporting } = useGridExport();
return (
<div className="toolbar">
<button onClick={() => exportToCsv('employees.csv')} disabled={isExporting()}>
Export CSV
</button>
<button onClick={selectAll}>Select All</button>
<button onClick={() => console.log(getSelectedRows())}>Log Selection</button>
<button onClick={clearSelection}>Clear</button>
</div>
);
}

When a component contains multiple grids, pass a CSS selector to target a specific one:

import { useGrid } from '@toolbox-web/grid-react';
import { useGridSelection } from '@toolbox-web/grid-react/features/selection';
function MultiGridPage() {
// Target grids by selector instead of ref/context
const primaryGrid = useGrid('#primary-grid');
const selection = useGridSelection('#primary-grid');
return (
<div>
<DataGrid id="primary-grid" rows={employees} selection="row" />
<DataGrid id="secondary-grid" rows={departments} />
</div>
);
}

Register application-wide renderers for data types using GridTypeProvider (see GridTypeProviderProps) with a TypeDefaultsMap:

import { GridTypeProvider, DataGrid, type TypeDefaultsMap } from '@toolbox-web/grid-react';
// Define type defaults for your application
const typeDefaults: TypeDefaultsMap = {
currency: {
format: (value: number) => '$' + value.toLocaleString(),
renderer: ({ value }) => (
<span style={{ color: value < 0 ? 'red' : 'green' }}>{value}</span>
),
},
date: {
format: (value: string) => new Date(value).toLocaleDateString(),
},
boolean: {
renderer: ({ value }) => <span>{value ? '' : ''}</span>,
},
};
// Wrap your app (or a section) with the provider
function App() {
return (
<GridTypeProvider defaults={typeDefaults}>
<EmployeeGrid />
</GridTypeProvider>
);
}
// Now columns with these types use the defaults automatically
function EmployeeGrid() {
return (
<DataGrid
rows={employees}
columns={[
{ field: 'salary', header: 'Salary', type: 'currency' }, // Uses currency format/renderer
{ field: 'hireDate', header: 'Hire Date', type: 'date' }, // Uses date format
{ field: 'isActive', header: 'Active', type: 'boolean' }, // Uses boolean renderer
]}
/>
);
}

Feature imports export scoped hooks for type-safe programmatic access to plugin functionality:

import '@toolbox-web/grid-react/features/export';
import { useGridExport } from '@toolbox-web/grid-react/features/export';
import { DataGrid } from '@toolbox-web/grid-react';
function EmployeeGrid() {
const { exportToCsv, exportToExcel, isExporting } = useGridExport();
return (
<div>
<button onClick={() => exportToCsv('employees.csv')} disabled={isExporting()}>
Export CSV
</button>
<DataGrid rows={employees} columns={columns} export />
</div>
);
}

See the API Reference > Hooks section for detailed method signatures, return types, and examples.

HookImportKey Methods
useGridSelection()features/selectionselectAll, clearSelection, getSelection, getSelectedRows
useGridFiltering()features/filteringsetFilter, clearAllFilters, getFilters, getFilteredRowCount
useGridExport()features/exportexportToCsv, exportToExcel, exportToJson, isExporting
useGridPrint()features/printprint, isPrinting
useGridUndoRedo()features/undo-redoundo, redo, canUndo, canRedo

Custom React editors that open a popover, listbox, calendar, or other floating panel typically render that panel into a portal so it can escape the cell’s overflow clipping. Without help, the grid sees a click into the portal as a click outside the editor and commits-and-exits the row before the user can pick an option.

The useGridOverlay hook bridges that gap by registering the panel with the grid as an external focus container — the React equivalent of Angular’s BaseOverlayEditor.initOverlay(). Pair it with aria-expanded / aria-controls on the trigger so even editors that forget to call the hook get correct keyboard / pointer behaviour out of the box (the editing plugin honours those attributes as a generic fallback — see issue #251).

import { createPortal } from 'react-dom';
import { useId, useRef, useState } from 'react';
import { useGridOverlay, type GridEditorContext } from '@toolbox-web/grid-react';
function AutocompleteEditor({ value, commit, cancel }: GridEditorContext<MyRow>) {
const [open, setOpen] = useState(false);
const panelRef = useRef<HTMLDivElement | null>(null);
const listboxId = useId();
// Registers panelRef.current with the grid while open=true; unregisters
// automatically on close or unmount.
useGridOverlay(panelRef, { open });
return (
<>
<input
role="combobox"
aria-expanded={open}
aria-controls={listboxId}
defaultValue={String(value ?? '')}
onClick={() => setOpen(true)}
onKeyDown={(e) => {
if (e.key === 'Escape') cancel();
}}
/>
{open &&
createPortal(
<div ref={panelRef} id={listboxId} role="listbox" className="my-listbox">
{/* options that call commit(option) on click */}
</div>,
document.body,
)}
</>
);
}

useGridOverlay(panelRef, options?) resolves the owning <tbw-grid> in this order:

  1. options.gridElement if explicitly passed,
  2. panelRef.current.closest('tbw-grid') for inline (non-portaled) panels,
  3. The GridElementContext populated by <DataGrid> / <GridProvider>.

The grid host is also exposed on ColumnEditorContext.grid for editors that need to call grid APIs directly without using the hook.

Feature props are the recommended path. Use manual plugin instantiation only when you need a third-party plugin or a configuration the feature prop doesn’t expose. Feature props and manual plugins can be mixed — the grid merges them.

import { useMemo } from 'react';
import { DataGrid, type GridConfig } from '@toolbox-web/grid-react';
import { SelectionPlugin } from '@toolbox-web/grid/plugins/selection';
import { MyCustomPlugin } from './my-custom-plugin';
function EmployeeGrid() {
const config = useMemo<GridConfig<Employee>>(() => ({
columns: [
{ field: 'id', header: 'ID' },
{ field: 'name', header: 'Name', editable: true },
],
plugins: [
new SelectionPlugin({ mode: 'range', checkbox: true }),
new MyCustomPlugin({ /* third-party config */ }),
],
}), []);
return <DataGrid rows={employees} gridConfig={config} />;
}

Important: Always use useMemo for config objects to prevent re-creating them on every render.

The grid supports two complementary ways to customize icons — see the core Icon Customization reference and the Theming Guide for the full picture:

  • CSS variables (--tbw-icon-*) — preferred for themes and static customization; no JavaScript needed.
  • gridConfig.icons — for dynamic icons, icon libraries, or HTMLElement instances; takes precedence over CSS.

GridIconProvider is the React wrapper for the JS path — it injects icons into gridConfig.icons for every descendant <DataGrid>:

import { GridIconProvider, DataGrid } from '@toolbox-web/grid-react';
function App() {
return (
<GridIconProvider icons={{ sortAsc: '', sortDesc: '' }}>
<DataGrid rows={employees} columns={columns} />
</GridIconProvider>
);
}

For backends that own paging / sorting / filtering, use the serverSide feature prop. Your getRows handler receives the current sortModel, filterModel, and block range — return rows and the total count. See ServerSideDataSource, GetRowsParams, and GetRowsResult for the full contract — including the params.signal AbortSignal you can forward to fetch() for explicit cancellation.

import '@toolbox-web/grid-react/features/server-side';
import '@toolbox-web/grid-react/features/multi-sort';
import { useMemo } from 'react';
import { DataGrid } from '@toolbox-web/grid-react';
import type { ServerSideDataSource } from '@toolbox-web/grid/plugins/server-side';
function ServerSideGrid() {
const dataSource = useMemo<ServerSideDataSource<Employee>>(() => ({
getRows: async ({ startNode, endNode, sortModel, filterModel, signal }) => {
const params = new URLSearchParams({
from: String(startNode),
to: String(endNode),
});
if (sortModel?.length) {
params.set(
'sort',
sortModel.map((s) => `${s.field}:${s.direction}`).join(','),
);
}
if (filterModel) params.set('filter', JSON.stringify(filterModel));
const response = await fetch(`/api/employees?${params}`, { signal });
const { rows, totalNodeCount } = await response.json();
return { rows, totalNodeCount };
},
}), []);
return (
<DataGrid
columns={columns}
multiSort
serverSide={{ dataSource, cacheBlockSize: 100 }}
/>
);
}

If your app already uses TanStack Query, one useful pairing is to back ServerSidePlugin with React Query’s request cache. Each block the grid asks for becomes a query keyed by the request shape (startNode, endNode, sortModel, filterModel) — so scrolling back to a previously fetched range is served from cache instead of re-hitting the API, and you get retries, in-flight deduplication, and devtools for free.

The pattern: keep getRows thin and delegate fetching to queryClient.fetchQuery with a stable key per block.

import '@toolbox-web/grid-react/features/server-side';
import '@toolbox-web/grid-react/features/multi-sort';
import { useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { DataGrid } from '@toolbox-web/grid-react';
import type {
GetRowsParams,
GetRowsResult,
ServerSideDataSource,
} from '@toolbox-web/grid/plugins/server-side';
async function fetchEmployeeBlock(
params: GetRowsParams,
): Promise<GetRowsResult<Employee>> {
const search = new URLSearchParams({
from: String(params.startNode),
to: String(params.endNode),
});
if (params.sortModel?.length) {
search.set('sort', params.sortModel.map((s) => `${s.field}:${s.direction}`).join(','));
}
if (params.filterModel) search.set('filter', JSON.stringify(params.filterModel));
const response = await fetch(`/api/employees?${search}`, { signal: params.signal });
return response.json();
}
function ServerSideGrid() {
const queryClient = useQueryClient();
const dataSource = useMemo<ServerSideDataSource<Employee>>(() => ({
// Each block becomes a cached query — React Query dedupes in-flight
// requests, retries failed ones, and serves repeated scrolls from cache.
getRows: (params) =>
queryClient.fetchQuery({
queryKey: [
'employees',
params.startNode,
params.endNode,
params.sortModel,
params.filterModel,
],
queryFn: ({ signal }) => fetchEmployeeBlock({ ...params, signal }),
staleTime: 60_000,
}),
}), [queryClient]);
return (
<DataGrid
columns={columns}
multiSort
serverSide={{ dataSource, cacheBlockSize: 100 }}
/>
);
}
import { useState, useCallback } from 'react';
import { DataGrid } from '@toolbox-web/grid-react';
function EmployeeGrid() {
const [employees, setEmployees] = useState<Employee[]>(initialData);
const handleAdd = useCallback(() => {
setEmployees(prev => [
...prev,
{ id: Date.now(), name: 'New Employee', department: '', salary: 50000 },
]);
}, []);
const handleDelete = useCallback(() => {
// Remove first row as example
setEmployees(prev => prev.slice(1));
}, []);
return (
<div>
<button onClick={handleAdd}>Add</button>
<button onClick={handleDelete}>Remove First</button>
<DataGrid rows={employees} columns={columns} />
</div>
);
}
// ✅ Good: Memoized — stable reference
const config = useMemo<GridConfig<Employee>>(() => ({
columns: [/* ... */],
}), []);
// ❌ Bad: Creates new config every render
const config: GridConfig<Employee> = { columns: [/* ... */] };
const StatusBadge = React.memo(function StatusBadge({ status }: { status: string }) {
return <span className={`badge badge--${status}`}>{status}</span>;
});

Ensure you imported the feature side-effect:

// ❌ This won't work
<DataGrid selection="range" />
// ✅ Import the feature first
import '@toolbox-web/grid-react/features/selection';
<DataGrid selection="range" />

Wrap in useMemo, or use feature props directly:

// ✅ Best: Feature props — no config object needed
<DataGrid selection="row" />
// ✅ Also good: Stable config reference
const config = useMemo(() => ({ features: { selection: true } }), []);

Create new arrays — the grid uses reference equality:

// ❌ Bad: Mutating existing array
employees.push(newEmployee);
setEmployees(employees);
// ✅ Good: New array reference
setEmployees([...employees, newEmployee]);
AI assistants: For complete API documentation, implementation guides, and code examples for this library, see https://raw.githubusercontent.com/OysteinAmundsen/toolbox/main/llms-full.txt