React Adapter
The @gridstorm/react package provides a first-class React wrapper around the GridStorm headless engine. It includes the <GridStorm> component, reactive hooks powered by useSyncExternalStore, a portal system for rendering React components inside grid cells, and full support for controlled and uncontrolled state patterns.
Installation
npm install @gridstorm/core @gridstorm/dom-renderer @gridstorm/react @gridstorm/theme-default
Import the default theme CSS in your application entry point:
import '@gridstorm/theme-default';
Basic Usage
import { GridStorm } from '@gridstorm/react';
import type { ReactColumnDef } from '@gridstorm/react';
interface Employee {
id: string;
name: string;
department: string;
salary: number;
}
const columns: ReactColumnDef<Employee>[] = [
{ field: 'name', headerName: 'Name', width: 200 },
{ field: 'department', headerName: 'Department', width: 150 },
{ field: 'salary', headerName: 'Salary', width: 120 },
];
const rowData: Employee[] = [
{ id: '1', name: 'Alice', department: 'Engineering', salary: 95000 },
{ id: '2', name: 'Bob', department: 'Design', salary: 85000 },
];
export function EmployeeGrid() {
return (
<GridStorm<Employee>
columns={columns}
rowData={rowData}
getRowId={(params) => params.data.id}
height={400}
/>
);
}
:::example{title="React Integration Demo" href="/react-demo/"} See the full React integration with hooks, custom cell renderers, portals, and error boundaries. :::
Props Reference
The <GridStorm> component accepts all core GridConfig props plus React-specific props for layout, controlled state, events, and renderer configuration.
Core Grid Props
| Name | Type | Description |
|---|---|---|
columns |
ReactColumnDef<TData>[] |
Column definitions. Supports React component renderers. |
rowData |
TData[] |
Client-side row data array. |
dataSource |
DataSource |
Server-side data source (alternative to rowData). |
rowModelType |
string |
Row model type. Default: 'clientSide'. |
getRowId |
(params) => string |
Callback to generate a unique ID for each row. |
plugins |
GridPlugin[] |
Array of plugins to register during initialization. |
defaultColDef |
Partial<ColumnDef> |
Default column definition applied to all columns. |
rowHeight |
number |
Row height in pixels. |
headerHeight |
number |
Header row height in pixels. |
domLayout |
'normal' | 'autoHeight' | 'print' |
Controls how the grid DOM height is determined. |
pinnedTopRowData |
TData[] |
Rows pinned to the top of the grid. |
pinnedBottomRowData |
TData[] |
Rows pinned to the bottom of the grid. |
suppressScrollX |
boolean |
Suppress horizontal scrollbar. |
suppressScrollY |
boolean |
Suppress vertical scrollbar. |
rowSelection |
'single' | 'multiple' | false |
Row selection mode. |
editType |
string |
Cell editing type. |
undoRedoCellEditing |
boolean |
Enable undo/redo for cell editing. |
pagination |
boolean |
Enable pagination. |
paginationPageSize |
number |
Rows per page. |
animateRows |
boolean |
Enable row animation on sort/filter changes. |
ariaLabel |
string |
ARIA label for the grid root element. |
locale |
string |
Locale identifier for i18n. |
theme |
string |
Theme name applied to the grid. |
Component Layout Props
| Name | Type | Default | Description |
|---|---|---|---|
height |
number | string |
400 |
Container height. Numbers are treated as pixels. |
width |
number | string |
'100%' |
Container width. Numbers are treated as pixels. |
containerClass |
string |
undefined |
Additional CSS class on the container <div>. |
containerStyle |
CSSProperties |
undefined |
Additional inline styles on the container. |
contextMenu |
ReactContextMenu<TData> |
undefined |
Custom context menu React component. |
children |
ReactNode |
undefined |
Children rendered inside the GridContext.Provider. |
Renderer Config Props
| Name | Type | Default | Description |
|---|---|---|---|
enableCellEditing |
boolean |
auto-detect | Enable inline cell editing overlay. |
enableGrouping |
boolean |
auto-detect | Enable row grouping visuals (chevron, indent, group label). |
groupIndent |
number |
24 |
Indentation per group level in pixels. |
checkboxSelection |
boolean |
false |
Show checkbox selection column as the first column. |
checkboxColumnWidth |
number |
48 |
Width of the checkbox column in pixels. |
floatingFilter |
boolean |
false |
Show floating filter inputs below the header. |
floatingFilterDebounce |
number |
300 |
Debounce delay for filter input in milliseconds. |
enablePagination |
boolean |
auto-detect | Show pagination bar below the grid. |
pageSizeOptions |
number[] |
[25, 50, 100, 250] |
Available page size options for the page size selector. |
Controlled State Props
| Name | Type | Description |
|---|---|---|
sortModel |
SortModelItem[] |
Controlled sort model. Grid will not update sort internally. |
onSortModelChange |
(sortModel) => void |
Called when the user changes sort in controlled mode. |
filterModel |
Record<string, FilterModel> |
Controlled filter model. |
onFilterModelChange |
(filterModel) => void |
Called when the user changes filters in controlled mode. |
selectedRowIds |
Set<string> |
Controlled selection (set of row IDs). |
onSelectedRowIdsChange |
(ids, source) => void |
Called when the user changes selection in controlled mode. |
currentPage |
number |
Controlled pagination page (0-indexed). |
onCurrentPageChange |
(page) => void |
Called when the user changes page in controlled mode. |
Event Callback Props
| Name | Core Event | Description |
|---|---|---|
onGridReady |
-- | Fires when the engine is initialized. Receives GridApi. |
onRowDataChanged |
rowData:changed |
Row data was updated. |
onSelectionChanged |
selection:changed |
Selection state changed. |
onSortChanged |
column:sort:changed |
Sort model changed. |
onFilterChanged |
filter:changed |
Filter model changed. |
onCellValueChanged |
cell:valueChanged |
A cell value was edited. |
onCellClicked |
cell:clicked |
A cell was clicked. |
onCellDoubleClicked |
cell:doubleClicked |
A cell was double-clicked. |
onRowClicked |
row:clicked |
A row was clicked. |
onCellEditingStarted |
cell:editingStarted |
Cell editing began. |
onCellEditingStopped |
cell:editingStopped |
Cell editing ended. |
onPaginationChanged |
pagination:changed |
Pagination state changed. |
onColumnResized |
column:resized |
A column was resized. |
Hooks
All hooks must be called inside a child of <GridStorm>, where they have access to the GridContext. They use useSyncExternalStore internally, so they are SSR-compatible and React StrictMode-safe.
useGridApi
Returns the GridApi instance for imperative actions.
import { useGridApi } from '@gridstorm/react';
function ExportButton() {
const api = useGridApi();
return (
<button onClick={() => console.log(api.getSelectedRows())}>
Log Selected
</button>
);
}
| Return | Type | Description |
|---|---|---|
api |
GridApi<TData> |
The grid API instance. |
useGridState
Selector-based reactive state subscription. Re-renders only when the selected value changes (reference equality).
import { useGridState } from '@gridstorm/react';
function RowCounter() {
const rowCount = useGridState((state) => state.displayedRowIds.length);
return <span>{rowCount} rows</span>;
}
| Parameter | Type | Description |
|---|---|---|
selector |
(state: GridState<TData>) => TResult |
Selector function that extracts a value from grid state. |
| Return | Type | Description |
|---|---|---|
result |
TResult |
The selected state value. |
useGridColumn
Access column state and manipulation actions.
import { useGridColumn } from '@gridstorm/react';
function ColumnToggle({ colId }: { colId: string }) {
const { visibleColumns, setColumnVisible, getColumn } = useGridColumn();
const col = getColumn(colId);
return (
<label>
<input
type="checkbox"
checked={!col?.hide}
onChange={(e) => setColumnVisible(colId, e.target.checked)}
/>
{col?.headerName}
</label>
);
}
| Return Property | Type | Description |
|---|---|---|
allColumns |
ColumnState[] |
All columns including hidden ones. |
visibleColumns |
ColumnState[] |
Only visible columns. |
setColumnVisible |
(colId, visible) => void |
Toggle column visibility. |
setColumnWidth |
(colId, width) => void |
Set a column's width. |
moveColumn |
(colId, toIndex) => void |
Move a column to a new index. |
setColumnPinned |
(colId, pinned) => void |
Pin a column ('left', 'right', or null). |
getColumn |
(colId) => ColumnState | undefined |
Get a single column by ID. |
useGridSort
Access sort model state and actions.
import { useGridSort } from '@gridstorm/react';
function SortControls() {
const { sortModel, isSorted, toggleSort, clearSort } = useGridSort();
return (
<div>
<button onClick={() => toggleSort('name')}>Sort by Name</button>
<button onClick={() => toggleSort('salary', true)}>Add Salary Sort</button>
{isSorted && <button onClick={clearSort}>Clear Sort</button>}
</div>
);
}
| Return Property | Type | Description |
|---|---|---|
sortModel |
SortModelItem[] |
Current sort model array. |
isSorted |
boolean |
Whether any sort is active. |
setSortModel |
(model) => void |
Set the sort model directly. |
toggleSort |
(colId, multiSort?) => void |
Toggle sort on a column. Pass true for multi-sort. |
clearSort |
() => void |
Clear all sorting. |
useGridFilter
Access filter model state and actions.
import { useGridFilter } from '@gridstorm/react';
function QuickSearch() {
const { isFiltered, setQuickFilter, clearFilters } = useGridFilter();
return (
<div>
<input
placeholder="Search..."
onChange={(e) => setQuickFilter(e.target.value)}
/>
{isFiltered && <button onClick={clearFilters}>Clear</button>}
</div>
);
}
| Return Property | Type | Description |
|---|---|---|
filterModel |
Record<string, FilterModel> |
Current filter model keyed by column ID. |
quickFilterText |
string |
Current quick filter text. |
isFiltered |
boolean |
Whether any filter is active. |
setFilterModel |
(model) => void |
Set the filter model. |
setQuickFilter |
(text) => void |
Set the quick filter text. |
clearFilters |
() => void |
Clear all filters and quick filter text. |
useGridPagination
Access pagination state and navigation actions.
import { useGridPagination } from '@gridstorm/react';
function Pager() {
const {
currentPage, totalPages, hasNextPage, hasPreviousPage,
nextPage, previousPage, firstPage, lastPage,
} = useGridPagination();
return (
<nav>
<button onClick={firstPage} disabled={!hasPreviousPage}>First</button>
<button onClick={previousPage} disabled={!hasPreviousPage}>Prev</button>
<span>Page {currentPage + 1} of {totalPages}</span>
<button onClick={nextPage} disabled={!hasNextPage}>Next</button>
<button onClick={lastPage} disabled={!hasNextPage}>Last</button>
</nav>
);
}
| Return Property | Type | Description |
|---|---|---|
currentPage |
number |
Current page (0-indexed). |
totalPages |
number |
Total number of pages. |
pageSize |
number |
Rows per page. |
totalRows |
number |
Total row count after filtering. |
hasNextPage |
boolean |
Whether there is a next page. |
hasPreviousPage |
boolean |
Whether there is a previous page. |
goToPage |
(page) => void |
Go to a specific page. |
nextPage |
() => void |
Go to the next page. |
previousPage |
() => void |
Go to the previous page. |
firstPage |
() => void |
Go to the first page. |
lastPage |
() => void |
Go to the last page. |
useGridSelection
Access selection state and actions.
import { useGridSelection } from '@gridstorm/react';
function SelectionInfo() {
const { selectedCount, selectAll, deselectAll, getSelectedRows } =
useGridSelection<Employee>();
return (
<div>
<span>{selectedCount} selected</span>
<button onClick={selectAll}>Select All</button>
<button onClick={deselectAll}>Deselect All</button>
<button onClick={() => console.log(getSelectedRows())}>Log Data</button>
</div>
);
}
| Return Property | Type | Description |
|---|---|---|
selectedRowIds |
Set<string> |
Set of selected row IDs. |
selectedCount |
number |
Number of selected rows. |
getSelectedRows |
() => TData[] |
Get selected row data objects. |
getSelectedNodes |
() => RowNode<TData>[] |
Get selected RowNode objects. |
isRowSelected |
(rowId) => boolean |
Check if a specific row is selected. |
selectAll |
() => void |
Select all visible rows. |
deselectAll |
() => void |
Deselect all rows. |
useGridEvent
Subscribe to typed grid events. The handler ref prevents stale closures without re-subscribing.
import { useGridEvent } from '@gridstorm/react';
function CellClickLogger() {
useGridEvent('cell:clicked', (event) => {
console.log('Clicked:', event.colId, event.value);
});
return null;
}
| Parameter | Type | Description |
|---|---|---|
event |
keyof GridEventMap |
The event name to subscribe to. |
handler |
(payload) => void |
Callback invoked when the event fires. |
Custom Cell Renderers
You can use React components as cell renderers. The PortalManager automatically detects React renderers and portals them into grid cells.
import type { CellRendererProps } from '@gridstorm/react';
function StatusBadge({ value }: CellRendererProps<Employee, string>) {
const color = value === 'Active' ? 'green' : 'gray';
return <span style={{ color, fontWeight: 600 }}>{value}</span>;
}
const columns: ReactColumnDef<Employee>[] = [
{ field: 'name', headerName: 'Name' },
{ field: 'status', headerName: 'Status', cellRenderer: StatusBadge },
];
CellRendererProps Reference
| Property | Type | Description |
|---|---|---|
value |
TValue |
The cell's current value (after valueGetter). |
formattedValue |
string |
The formatted display value (after valueFormatter). |
data |
TData | undefined |
The row data object. |
node |
RowNode<TData> |
The RowNode for this row. |
colDef |
ColumnDef<TData, TValue> |
The column definition. |
colId |
string |
Column ID. |
rowIndex |
number |
Display row index. |
api |
GridApi<TData> |
Grid API reference. |
Custom Header Renderers
import type { HeaderRendererProps } from '@gridstorm/react';
function SortableHeader({ displayName, sortDirection, onSortRequested }: HeaderRendererProps) {
const icon = sortDirection === 'asc' ? ' ^' : sortDirection === 'desc' ? ' v' : '';
return (
<div onClick={() => onSortRequested(false)}>
{displayName}{icon}
</div>
);
}
HeaderRendererProps Reference
| Property | Type | Description |
|---|---|---|
colDef |
ColumnDef<TData> |
Column definition. |
colId |
string |
Column ID. |
displayName |
string |
Display name for the header. |
sortDirection |
SortDirection |
Current sort direction ('asc', 'desc', or null). |
sortIndex |
number | null |
Sort priority index for multi-sort. |
api |
GridApi<TData> |
Grid API reference. |
onSortRequested |
(multiSort: boolean) => void |
Request a sort toggle on this column. |
Custom Cell Editors
import type { CellEditorProps } from '@gridstorm/react';
function NumericEditor({ value, onValueChange, stopEditing }: CellEditorProps<Employee, number>) {
return (
<input
type="number"
defaultValue={value}
autoFocus
onChange={(e) => onValueChange(Number(e.target.value))}
onKeyDown={(e) => {
if (e.key === 'Enter') stopEditing();
if (e.key === 'Escape') stopEditing(true);
}}
/>
);
}
const columns: ReactColumnDef<Employee>[] = [
{ field: 'salary', headerName: 'Salary', editable: true, cellEditorComponent: NumericEditor },
];
CellEditorProps Reference
| Property | Type | Description |
|---|---|---|
value |
TValue |
Current cell value. |
data |
TData |
Row data object. |
colId |
string |
Column ID. |
rowId |
string |
Row ID. |
column |
ColumnState |
Column state object. |
editorParams |
Record<string, unknown> |
Additional editor params from column def. |
onValueChange |
(value: TValue) => void |
Callback to update the value while editing. |
stopEditing |
(cancel?: boolean) => void |
Stop editing. Pass true to cancel without saving. |
api |
GridApi<TData> |
Grid API reference. |
Context Menu Component
import type { ContextMenuProps } from '@gridstorm/react';
function MyMenu({ node, colId, value, api, closeMenu }: ContextMenuProps) {
return (
<div className="gs-context-menu">
<button
className="gs-context-menu-item"
onClick={() => {
navigator.clipboard.writeText(String(value));
closeMenu();
}}
>
Copy Value
</button>
<button className="gs-context-menu-item" onClick={closeMenu}>
Close
</button>
</div>
);
}
<GridStorm columns={columns} rowData={rowData} contextMenu={MyMenu} />
ContextMenuProps Reference
| Property | Type | Description |
|---|---|---|
position |
CellPosition |
The cell position that was right-clicked. |
node |
RowNode<TData> |
The row node. |
colId |
string |
Column ID. |
value |
any |
Cell value. |
api |
GridApi<TData> |
Grid API reference. |
closeMenu |
() => void |
Close the context menu. |
Portal System
The PortalManager is the mechanism that bridges React components with the DOM-based grid renderer. You do not need to configure it manually -- it is included automatically when you use the <GridStorm> component.
How it works:
- A
MutationObserverwatches the.gs-bodyelement for DOM changes fromDomRenderer. - When rows are added or removed, the
PortalManagerscans visible cells for columns that have React renderers. - For each matching cell, a stable wrapper
<div style="display:contents">is created inside the cell, and a React portal renders the component into that wrapper. - When
DomRendererdestroys or recycles cells during virtual scrolling, the wrapper is detached but React can still safely unmount its children. - Header renderer portals work the same way, re-scanning when sort or column changes occur.
- Editor portals are created when
cell:editingStartedfires and destroyed whencell:editingStoppedfires. - Context menu portals are created on right-click and destroyed when the menu is closed.
Controlled vs Uncontrolled Modes
By default, GridStorm manages its own state (uncontrolled). You can take control of specific state slices by providing the controlled props and their corresponding change handlers.
Uncontrolled (default)
<GridStorm
columns={columns}
rowData={rowData}
onSortChanged={(e) => console.log('Sort changed:', e.sortModel)}
/>
Controlled Sort
import { useState } from 'react';
import type { SortModelItem } from '@gridstorm/core';
function ControlledSortGrid() {
const [sortModel, setSortModel] = useState<SortModelItem[]>([]);
return (
<GridStorm
columns={columns}
rowData={rowData}
sortModel={sortModel}
onSortModelChange={(newModel) => {
// Validate, transform, or persist before applying
setSortModel(newModel);
}}
/>
);
}
Controlled Selection
function ControlledSelectionGrid() {
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
return (
<GridStorm
columns={columns}
rowData={rowData}
rowSelection="multiple"
selectedRowIds={selectedIds}
onSelectedRowIdsChange={(ids, source) => {
console.log('Selection source:', source);
setSelectedIds(ids);
}}
/>
);
}
Controlled Pagination
function ControlledPaginationGrid() {
const [page, setPage] = useState(0);
return (
<GridStorm
columns={columns}
rowData={rowData}
pagination
paginationPageSize={25}
currentPage={page}
onCurrentPageChange={setPage}
/>
);
}
Controlled Filter
function ControlledFilterGrid() {
const [filterModel, setFilterModel] = useState({});
return (
<GridStorm
columns={columns}
rowData={rowData}
filterModel={filterModel}
onFilterModelChange={(newModel) => {
setFilterModel(newModel);
}}
/>
);
}
Children and Context
The <GridStorm> component provides a React context via GridContext.Provider. Any children rendered inside it can use all GridStorm hooks:
<GridStorm columns={columns} rowData={data} plugins={plugins}>
<Toolbar /> {/* Can use useGridApi(), useGridSort(), etc. */}
<StatusBar />
<PaginationBar />
</GridStorm>
SSR Support
All hooks use useSyncExternalStore with a getServerSnapshot parameter that returns the same snapshot as the client. The grid engine is created in a useEffect, so it only runs on the client. During SSR, the component renders an empty container <div>.
React StrictMode Compatibility
The <GridStorm> component is fully compatible with React StrictMode. The internal useGridEngine hook creates the engine in a useEffect so that StrictMode's cleanup-then-remount cycle creates a fresh engine each time. The previous engine is destroyed in the cleanup function, preventing memory leaks.
Error Boundary
The <GridStorm> component wraps itself in a GridErrorBoundary that catches rendering errors and displays a fallback UI instead of crashing the entire application.
TypeScript Generics
The <GridStorm> component and all hooks accept a generic TData parameter for row data typing:
interface Trade {
id: string;
symbol: string;
price: number;
quantity: number;
}
// Component is fully typed
<GridStorm<Trade>
columns={[
{ field: 'symbol', headerName: 'Symbol' },
{ field: 'price', headerName: 'Price' },
]}
rowData={trades}
getRowId={(p) => p.data.id}
/>
// Hooks infer TData from context
const rows = useGridSelection<Trade>().getSelectedRows();
// rows is Trade[]
Next Steps
- Theming -- customize the grid appearance with CSS tokens
- Columns -- learn about column definitions and features
- Events & Commands -- listen to grid events and dispatch commands
- Vanilla JS -- use GridStorm without a framework
- Angular -- use GridStorm with Angular