Skip to content

React Integration

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

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 }}

These props control grid-wide behavior and don’t require feature imports:

PropTypeDefaultDescription
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 />

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 component
function 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" />;
}

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 component
function 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" />;
}

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}
/>
);
}

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>
);
}

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>
);
}

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>
);
}

Register application-wide renderers for data types using GridTypeProvider:

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>
);
}
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

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>
);
}

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 useMemo for config objects to prevent re-creating them on every render.

Feature props and manual plugins can be mixed — the grid merges them.

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>
);
}
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 />
);
}
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} />;
}
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