React Integration
The @toolbox-web/grid-react package provides React integration for the <tbw-grid> data grid component.
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 }} |
Core Config Props (no import needed)
Section titled “Core Config Props (no import needed)”These props control grid-wide behavior and don’t require feature imports:
| Prop | Type | Default | Description |
|---|---|---|---|
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 Cell Renderers
Section titled “Custom Cell Renderers”Define custom renderers inline using the renderer property on columns:
import '@toolbox-web/grid-react/features/editing';import { DataGrid, type GridConfig } from '@toolbox-web/grid-react';
interface Employee { id: number; name: string; status: 'active' | 'inactive' | 'pending'; salary: number;}
// Status badge componentfunction StatusBadge({ status }: { status: string }) { const colors = { active: '#22c55e', inactive: '#94a3b8', pending: '#f59e0b', }; return ( <span style={{ padding: '4px 8px', borderRadius: '4px', background: colors[status as keyof typeof colors], color: status === 'pending' ? 'black' : 'white', }} > {status} </span> );}
function EmployeeGrid() { const config: GridConfig<Employee> = { columns: [ { field: 'id', header: 'ID', width: 70 }, { field: 'name', header: 'Name' }, { field: 'status', header: 'Status', // Use React components as renderers! renderer: ({ value }) => <StatusBadge status={value} />, }, { field: 'salary', header: 'Salary', type: 'number', // Format with a simple function for non-JSX format: (v) => '$' + v.toLocaleString(), }, ], };
return <DataGrid rows={employees} gridConfig={config} editing="dblclick" />;}Custom Cell Editors
Section titled “Custom Cell Editors”Define custom editors with the editor property. The editor receives commit and cancel callbacks:
import '@toolbox-web/grid-react/features/editing';import { DataGrid, type GridConfig, type GridEditorContext } from '@toolbox-web/grid-react';
// Custom select editor componentfunction StatusEditor({ value, commit, cancel }: GridEditorContext<string, Employee>) { return ( <select defaultValue={value} autoFocus onChange={(e) => commit(e.target.value)} onKeyDown={(e) => e.key === 'Escape' && cancel()} onBlur={() => cancel()} > <option value="active">Active</option> <option value="inactive">Inactive</option> <option value="pending">Pending</option> </select> );}
function EmployeeGrid() { const config: GridConfig<Employee> = { columns: [ { field: 'id', header: 'ID', width: 70 }, { field: 'name', header: 'Name', editable: true }, { field: 'status', header: 'Status', editable: true, renderer: ({ value }) => <StatusBadge status={value} />, editor: (ctx) => <StatusEditor {...ctx} />, }, ], };
return <DataGrid rows={employees} gridConfig={config} editing="dblclick" />;}Handling Events
Section titled “Handling Events”Use event handler props for clean, declarative event handling with automatic cleanup:
import '@toolbox-web/grid-react/features/selection';import '@toolbox-web/grid-react/features/editing';import '@toolbox-web/grid-react/features/multi-sort';import { DataGrid } from '@toolbox-web/grid-react';import type { CellClickDetail, CellCommitDetail, SortChangeDetail } from '@toolbox-web/grid';
function EmployeeGrid() { const handleCellClick = useCallback((detail: CellClickDetail<Employee>) => { console.log('Cell clicked:', detail.row, detail.column.field); }, []);
const handleCellCommit = useCallback((detail: CellCommitDetail<Employee>) => { console.log('Cell edited:', detail.column.field, detail.oldValue, '→', detail.newValue); }, []);
const handleSortChange = useCallback((detail: SortChangeDetail) => { console.log('Sort changed:', detail.sortState); }, []);
return ( <DataGrid rows={employees} columns={columns} selection="range" editing="dblclick" multiSort // Event props - automatically cleaned up on unmount onCellClick={handleCellClick} onCellCommit={handleCellCommit} onSortChange={handleSortChange} /> );}Master-Detail Panels
Section titled “Master-Detail Panels”Use GridDetailPanel for expandable row details:
import '@toolbox-web/grid-react/features/master-detail';import { DataGrid, GridDetailPanel, type DetailPanelContext } from '@toolbox-web/grid-react';
function EmployeeGrid() { return ( <DataGrid rows={employees} columns={columns} masterDetail> <GridDetailPanel> {({ row }: DetailPanelContext<Employee>) => ( <div style={{ padding: 16 }}> <h3>{row.name}</h3> <p>Department: {row.department}</p> <p>Email: {row.email}</p> <p>Hire Date: {new Date(row.hireDate).toLocaleDateString()}</p> </div> )} </GridDetailPanel> </DataGrid> );}Custom Tool Panels
Section titled “Custom Tool Panels”Add sidebar panels with GridToolPanel:
import { DataGrid, GridToolPanel, type ToolPanelContext } from '@toolbox-web/grid-react';
function EmployeeGrid() { return ( <DataGrid rows={employees} columns={columns} gridConfig={{ shell: { toolPanels: [{ id: 'filters', icon: '🔍', position: 'left' }], }, }} > <GridToolPanel id="filters"> {({ closePanel }: ToolPanelContext) => ( <div style={{ padding: 16 }}> <h3>Quick Filters</h3> <label> <input type="checkbox" /> Active Only </label> <button onClick={closePanel}>Close</button> </div> )} </GridToolPanel> </DataGrid> );}Using the useGrid Hook
Section titled “Using the useGrid Hook”The useGrid hook provides programmatic access to the grid:
import '@toolbox-web/grid-react/features/selection';import '@toolbox-web/grid-react/features/export';import { DataGrid, useGrid } from '@toolbox-web/grid-react';
function EmployeeGrid() { const { ref, // Ref to pass to DataGrid element, // Direct access to grid element forceLayout, // Force re-render getConfig, // Get current config exportToCsv, // Export helpers (when export feature loaded) getSelectedRows, // Selection helpers (when selection feature loaded) selectAll, clearSelection, } = useGrid<Employee>();
const handleExport = async () => { await exportToCsv({ filename: 'employees.csv' }); };
const handleSelectAll = () => { selectAll(); };
return ( <div> <div className="toolbar"> <button onClick={handleExport}>Export CSV</button> <button onClick={handleSelectAll}>Select All</button> <button onClick={() => console.log(getSelectedRows())}>Log Selection</button> </div> <DataGrid ref={ref} rows={employees} columns={columns} selection="range" export /> </div> );}Type-Level Defaults
Section titled “Type-Level Defaults”Register application-wide renderers for data types using GridTypeProvider:
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”| 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 |
The useGridEvent Hook
Section titled “The useGridEvent Hook”For programmatic event handling outside of props, use useGridEvent:
import { useRef, useState } from 'react';import { DataGrid, useGridEvent } from '@toolbox-web/grid-react';import type { GridElement, CellCommitDetail } from '@toolbox-web/grid';
function EmployeeGrid() { const gridRef = useRef<GridElement>(null); const [editHistory, setEditHistory] = useState<string[]>([]);
useGridEvent(gridRef, 'cell-commit', (detail: CellCommitDetail<Employee>) => { setEditHistory(prev => [ ...prev, `${detail.column.field}: ${detail.oldValue} → ${detail.newValue}` ]); });
return ( <div> <ul>{editHistory.map((entry, i) => <li key={i}>{entry}</li>)}</ul> <DataGrid ref={gridRef} rows={employees} columns={columns} editing="dblclick" /> </div> );}Manual Plugin Instantiation
Section titled “Manual Plugin Instantiation”For custom configurations or third-party plugins, instantiate plugins manually:
import { useMemo } from 'react';import { DataGrid, type GridConfig } from '@toolbox-web/grid-react';
function EmployeeGrid() { const config = useMemo<GridConfig<Employee>>(() => ({ columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name', editable: true }, ], features: { selection: { mode: 'range', checkbox: true }, editing: { editOn: 'dblclick' }, clipboard: { includeHeaders: true }, }, }), []);
return <DataGrid rows={employees} gridConfig={config} />;}Important: Always use
useMemofor config objects to prevent re-creating them on every render.
Feature props and manual plugins can be mixed — the grid merges them.
Custom Icons
Section titled “Custom Icons”Use GridIconProvider to override grid icons application-wide:
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”import { useState, useEffect, useCallback } from 'react';import { DataGrid, type GridConfig } from '@toolbox-web/grid-react';
function ServerSideGrid() { const [loading, setLoading] = useState(true); const [employees, setEmployees] = useState<Employee[]>([]);
const fetchData = useCallback(async (sortField?: string, sortDir?: string) => { setLoading(true); try { const params = new URLSearchParams(); if (sortField) params.set('sortField', sortField); if (sortDir) params.set('sortDir', sortDir); const response = await fetch(`/api/employees?${params}`); const { data } = await response.json(); setEmployees(data); } finally { setLoading(false); } }, []);
useEffect(() => { fetchData(); }, [fetchData]);
const config: GridConfig<Employee> = { columns, sortHandler: async (rows, sortState) => { const dir = sortState.direction === 1 ? 'asc' : 'desc'; const response = await fetch( `/api/employees?sortField=${sortState.field}&sortDir=${dir}` ); const { data } = await response.json(); return data; }, };
return ( <DataGrid rows={employees} gridConfig={config} multiSort /> );}React Query Integration
Section titled “React Query Integration”import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';import '@toolbox-web/grid-react/features/editing';import { DataGrid } from '@toolbox-web/grid-react';import type { CellCommitDetail } from '@toolbox-web/grid';
function EmployeeGrid() { const queryClient = useQueryClient();
const { data: employees = [], isLoading } = useQuery({ queryKey: ['employees'], queryFn: () => fetch('/api/employees').then(r => r.json()), });
const updateMutation = useMutation({ mutationFn: (employee: Partial<Employee>) => fetch(`/api/employees/${employee.id}`, { method: 'PATCH', body: JSON.stringify(employee), }), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['employees'] }), });
const handleCellCommit = useCallback((detail: CellCommitDetail<Employee>) => { updateMutation.mutate({ id: detail.row.id, [detail.column.field]: detail.newValue, }); }, [updateMutation]);
if (isLoading) return <div>Loading...</div>;
return <DataGrid rows={employees} columns={columns} editing="dblclick" onCellCommit={handleCellCommit} />;}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