React Integration
The @toolbox-web/grid-react package provides React integration for the <tbw-grid> data grid component.
Compatibility
Section titled “Compatibility”| React version | Support level |
|---|---|
| 19 | Tested — used in demos and CI |
| 18 | Tested |
| < 18 | Not supported (minimum peer dependency) |
Installation
Section titled “Installation”npm install @toolbox-web/grid @toolbox-web/grid-react
yarn add @toolbox-web/grid @toolbox-web/grid-reactpnpm add @toolbox-web/grid @toolbox-web/grid-reactbun add @toolbox-web/grid @toolbox-web/grid-reactRegister the grid web component in your application entry point:
// main.tsx or index.tsximport '@toolbox-web/grid';Basic Usage
Section titled “Basic Usage”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 }} /> );}Enabling Features with Props
Section titled “Enabling Features with Props”Features are enabled using declarative props combined with side-effect imports. This gives you clean JSX and tree-shakeable bundles.
How It Works
Section titled “How It Works”- Import the feature - A side-effect import registers the feature factory
- 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 /> );}Why Side-Effect Imports?
Section titled “Why Side-Effect Imports?”- 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
Available Features
Section titled “Available Features”| Import | Prop | Example |
|---|---|---|
features/selection | selection | selection="range" or selection={{ mode: 'row', checkbox: true }} |
features/multi-sort | multiSort | multiSort or multiSort={{ maxSortColumns: 3 }} |
features/filtering | filtering | filtering or filtering={{ debounceMs: 200 }} |
features/editing | editing | editing="dblclick" or editing="click" |
features/clipboard | clipboard | clipboard (requires selection) |
features/undo-redo | undoRedo | undoRedo (requires editing) |
features/context-menu | contextMenu | contextMenu |
features/reorder-columns | reorderColumns | reorderColumns (column drag-to-reorder) |
features/visibility | visibility | visibility (column visibility panel) |
features/pinned-columns | pinnedColumns | pinnedColumns |
features/grouping-columns | groupingColumns | groupingColumns |
features/grouping-rows | groupingRows | groupingRows={{ groupOn: (row) => row.department }} |
features/tree | tree | tree={{ childrenField: 'children' }} |
features/master-detail | masterDetail | masterDetail (use with <GridDetailPanel>) |
features/responsive | responsive | responsive (card layout on mobile) |
features/export | export | export |
features/print | print | print |
features/pinned-rows | pinnedRows | pinnedRows or pinnedRows={{ position: 'bottom' }} |
features/column-virtualization | columnVirtualization | columnVirtualization |
features/reorder-rows | reorderRows | reorderRows |
features/pivot | pivot | pivot={{ rowFields: ['category'], valueField: 'sales' }} |
features/server-side | serverSide | serverSide={{ pageSize: 50 }} |
features/row-drag-drop | rowDragDrop | rowDragDrop (cross-grid / external row drag-and-drop) |
features/tooltip | tooltip | tooltip (cell / header tooltips) |
Core Config Props (no import needed)
Section titled “Core Config Props (no import needed)”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.
| Prop | Type | Default | Description |
|---|---|---|---|
rows | T[] | [] | Row data — omit when using serverSide |
columns | ColumnConfig[] | — | Column definitions (alternative to gridConfig.columns) |
gridConfig | GridConfig | — | Full configuration object — always wrap in useMemo |
ref | Ref<DataGridRef> | — | Imperative handle (also returned by useGrid()) |
loading | boolean | false | Show the grid’s loading overlay |
fitMode | FitMode | 'auto' | Column-fit strategy |
customStyles | string | — | CSS injected into the grid (renderer/editor styling) |
ssr | boolean | false | Deprecated — no-op in practice; will be removed in a future major release |
sortable | boolean | true | Grid-wide sorting toggle. Set false to disable all sorting. |
filterable | boolean | true | Grid-wide filtering toggle. Requires FilteringPlugin. |
selectable | boolean | true | Grid-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}/>Import All Features (Development)
Section titled “Import All Features (Development)”For prototyping or when bundle size isn’t critical:
// Import all features at onceimport '@toolbox-web/grid-react/features';
// Now all feature props work<DataGrid selection="range" multiSort filtering editing="dblclick" clipboard />Custom Tool Panels
Section titled “Custom Tool Panels”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).
Using the useGrid Hook
Section titled “Using the useGrid Hook”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> );}Multiple Grids
Section titled “Multiple Grids”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> );}Type-Level Defaults
Section titled “Type-Level Defaults”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 applicationconst 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 providerfunction App() { return ( <GridTypeProvider defaults={typeDefaults}> <EmployeeGrid /> </GridTypeProvider> );}
// Now columns with these types use the defaults automaticallyfunction 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-Scoped Hooks
Section titled “Feature-Scoped Hooks”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> );}Available Feature Hooks
Section titled “Available Feature Hooks”See the API Reference > Hooks section for detailed method signatures, return types, and examples.
| Hook | Import | Key Methods |
|---|---|---|
useGridSelection() | features/selection | selectAll, clearSelection, getSelection, getSelectedRows |
useGridFiltering() | features/filtering | setFilter, clearAllFilters, getFilters, getFilteredRowCount |
useGridExport() | features/export | exportToCsv, exportToExcel, exportToJson, isExporting |
useGridPrint() | features/print | print, isPrinting |
useGridUndoRedo() | features/undo-redo | undo, redo, canUndo, canRedo |
Overlay Editors (useGridOverlay)
Section titled “Overlay Editors (useGridOverlay)”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:
options.gridElementif explicitly passed,panelRef.current.closest('tbw-grid')for inline (non-portaled) panels,- The
GridElementContextpopulated 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.
Manual Plugin Instantiation
Section titled “Manual Plugin Instantiation”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
useMemofor config objects to prevent re-creating them on every render.
Custom Icons
Section titled “Custom Icons”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, orHTMLElementinstances; 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> );}Server-Side Data Loading
Section titled “Server-Side Data Loading”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 }} /> );}React Query Integration
Section titled “React Query Integration”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 }} /> );}Dynamic Row Updates
Section titled “Dynamic Row Updates”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> );}Performance Tips
Section titled “Performance Tips”Memoize Config and Columns
Section titled “Memoize Config and Columns”// ✅ Good: Memoized — stable referenceconst config = useMemo<GridConfig<Employee>>(() => ({ columns: [/* ... */],}), []);
// ❌ Bad: Creates new config every renderconst config: GridConfig<Employee> = { columns: [/* ... */] };Use React.memo for Cell Components
Section titled “Use React.memo for Cell Components”const StatusBadge = React.memo(function StatusBadge({ status }: { status: string }) { return <span className={`badge badge--${status}`}>{status}</span>;});Troubleshooting
Section titled “Troubleshooting”Feature prop not working
Section titled “Feature prop not working”Ensure you imported the feature side-effect:
// ❌ This won't work<DataGrid selection="range" />
// ✅ Import the feature firstimport '@toolbox-web/grid-react/features/selection';<DataGrid selection="range" />Config object recreated on every render
Section titled “Config object recreated on every render”Wrap in useMemo, or use feature props directly:
// ✅ Best: Feature props — no config object needed<DataGrid selection="row" />
// ✅ Also good: Stable config referenceconst config = useMemo(() => ({ features: { selection: true } }), []);Grid not responding to row updates
Section titled “Grid not responding to row updates”Create new arrays — the grid uses reference equality:
// ❌ Bad: Mutating existing arrayemployees.push(newEmployee);setEmployees(employees);
// ✅ Good: New array referencesetEmployees([...employees, newEmployee]);See Also
Section titled “See Also”- API Reference — Complete API documentation for all components, hooks, and types
- Grid Plugins — All available plugins
- Core Features — Variable row heights, events, column config
- Common Patterns — Reusable recipes