Documentation
Guides

Coming from React Data Grid

Map React Data Grid's props, column options, renderers and examples onto this headless grid, and see what your app writes in their place.

This grid takes its API's shape from React Data Grid (7.0.0-beta.60): columns are plain objects, rows an array, the active cell and the sort are props, controlled or not. Most props keep their name. What changes is everything that renders: React Data Grid draws a grid with its own stylesheet, this one hands you the parts and draws nothing. This page maps each of React Data Grid's props and column options onto this grid, and says what your app does where the grid deliberately does nothing.

What changes first

  • No CSS, no icons, no text. There is no stylesheet to import and no rdg-light or rdg-dark class: every part takes your className and style, each a value or a function of the part's state ((state) => …). Light and dark are your own CSS. See Styling with Tailwind and Styling with plain CSS.
  • Parts instead of one component. <DataGrid /> becomes DataGrid.Root (the data and the scroll container) holding DataGrid.Grid, Header and Body; write the rows and cells out (Rows, Row, Cells, Cell) when you want to style or replace them. See The primitive contract.
  • The structure is yours. The same parts render divs or a real <table>: render replaces any part's element (an element, or a function of its props and state). See Tables or divs.
  • State is in attributes. The active cell, a selected row, a sorted header carry data-* attributes and ARIA (data-active, data-selected, data-sort, aria-sort), for your CSS. See State attributes.
  • No menus, no editors, no checkboxes. The grid owns the behaviour (which cell is edited, which rows are selected, which group is open) and your app renders the controls.
  • The grid writes no data. There is no onRowsChange: an edit, a paste, a fill and a row move are events, and your app writes its rows.
people.tsx
// React Data Grid
<DataGrid columns={columns} rows={people} rowKeyGetter={(person) => person.id} />

// this grid
<DataGrid.Root columns={columns} rows={people} rowKey={(person) => person.id} className="h-96">
    <DataGrid.Grid aria-label="People">
        <DataGrid.Header />
        <DataGrid.Body />
    </DataGrid.Grid>
</DataGrid.Root>

DataGrid.Root is the element that scrolls: give it a bounded height, as React Data Grid's container needs one. See Sizing the grid.

The grid's props

Rows and sizes

React Data Gridherenotes
rowsrowsthe same
—rowCount + getRow(index)rows by index: undefined is a row not loaded yet, which keeps its room and renders with data-loading. See Loading data
rowKeyGetter(row)rowKey(row, index)without one, a row's key is its index
rowHeight (a number, or a function of the row)rowHeighta number, a function of the row's index, or "auto": as tall as its content, measured (Measured heights)
headerRowHeightheaderRowHeightdefault 35 (not the row height); 0 for no header
summaryRowHeightsummaryRowHeightdefault 35
topSummaryRows, bottomSummaryRows (arrays of summary rows)summaryRows={{ top, bottom }} (counts)the figures are yours, read where the summary cells render. See Summary rows
enableVirtualization—always virtualized on both axes. To print or export, use the rows your app holds (rows, or useLocalRows's filteredRows); toTsv from @fragiola/data-grid makes tab-separated text of rows of strings, so map each row to its columns' texts first
directiondirection"ltr" or "rtl"; without it, the page's direction, as the browser computes it. See Right to left

Sorting and selection

React Data Gridherenotes
sortColumns, onSortColumnsChangethe same, plus defaultSortColumnsdirection is "ascending" or "descending" (not "ASC"/"DESC"). The grid never orders the rows: useLocalRows from @fragiola/data-grid-react/local does in memory, a server otherwise. See Sorting
selectedRows (a Set), onSelectedRowsChangeselectedRowKeys (an array), onSelectedRowKeysChange, defaultSelectedRowKeysturned on by rowSelection="single" or "multiple". See Row selection
isRowSelectionDisabled(row)isRowSelectable(row, rowIndex)the opposite question
onActivePositionChangeactivePosition, defaultActivePosition, onActivePositionChangethe position is { rowIndex, columnIndex } (header rows are -1 and above), or null
—cellSelection="range", selectedRange, onSelectedRangeChangea range of cells by the keys, a drag or Shift+click. See Cell selection

Editing, the clipboard and the fill handle

React Data Gridherenotes
onRowsChange(rows, data)onCellEdit, onRangePaste, onFill, onRowMoveeach tells what happened; your app writes the rows. Rows behind getRow are told with rows.changed
a column's edit committedonCellEdit({ row, rowIndex, columnIndex, columnKey, value })told only when the value changed. See Editing cells
onCellCopycellSelection="range" and a column's getCopyTextCtrl+C copies the range (or the active cell) as tab-separated text; your onCopy on Root runs first, and preventDefault makes the copy yours
onCellPaste (returns the row)onRangePaste({ range, values }), onBeforeRangePastethe values parsed from the clipboard and the range they land in; onBeforeRangePaste returning false refuses one
onFill({ columnKey, sourceRow, targetRow }) (returns the row)onFill({ source, target })two ranges of cells; giving onFill turns the handle on, and the handle is your element (useFillHandle). repeatedFill from @fragiola/data-grid/fill repeats the source's values. See Fill handle

Columns from the outside

React Data Gridherenotes
columnWidths (a Map), onColumnWidthsChangecolumnWidths ({ [columnKey]: px }), onColumnWidthsChange, defaultColumnWidthsonly the widths a person or your code set; a column's own width is the rest. See Column resizing
onColumnResize(column, width)onColumnWidthsChangeor engine.subscribe("column-resize", …) for the drag as it happens
onColumnsReorder(sourceKey, targetKey)columnOrder, onColumnOrderChange, defaultColumnOrderthe order is a list of keys; you never reorder columns yourself. See Column reordering
defaultColumnOptions—spread your own defaults into each column: columns.map((column) => ({ resizable: true, ...column }))

Events

React Data Gridherenotes
onCellClick, onCellDoubleClick, onCellContextMenu, onCellMouseDownonClick, onDoubleClick, onContextMenu, onPointerDown on DataGrid.Cellany prop a part does not use goes to its element; the cell's cell (its row, column and indexes) is in your closure. A menu is your own component (context-menu)
onCellKeyDown with preventGridDefault()onKeyDown on DataGrid.Cell with preventDefault()your handlers run before the grid's, so preventDefault cancels a grid key. See Replacing a key
moves refused in onCellKeyDowna middleware: model.use(…)it can refuse or rewrite any command, active-position.move and editing-cell.set included
onScrollonRowWindowChange, onColumnWindowChange, onRowsEndReachedthe rows and columns in view, and the end coming near. Beside the grid, useRowWindow(gridRef) and useColumnWindow(gridRef). onScroll on Root is still the element's own, but past the browser's size limit its scrollTop is not a row offset (Scroll scaling)

Rendering

React Data Gridherenotes
renderers.renderRowthe Rows children function, or render on DataGrid.Row(row) => <DataGrid.Row row={row}>…</DataGrid.Row>
renderers.renderCellthe Cells children function, or render on DataGrid.Cella cell given children renders only them
renderers.renderSortStatusa header cell's state (sortDirection, sortPriority) or data-sort, data-sort-priorityan arrow drawn in CSS, or in a render function
renderers.renderCheckboxyour own checkboxthe grid renders none
renderers.noRowsFallbackDataGrid.Emptyrendered in the body area while there are no rows. See Empty state
DataGridRenderersContexta component of yours around the partsthe parts are plain components: wrap them once and reuse them
rowClass(row, rowIndex)className on DataGrid.Row, a value or (state) => …the row's data is row.row in the Rows children function
headerRowClassclassName on DataGrid.HeaderRowthrough the HeaderRows children function
className, style, role, aria-*, data-*on Root or Gridname the grid on Grid (aria-label); the role is the grid's (grid, or treegrid with row kinds)

The ref

React Data Grid's ref is a handle with element, scrollToCell and setActivePosition (selectCell in earlier betas). Here ref stays the root's element, and a gridRef (useDataGridRef()) is the grid's model and engine, from outside the root:

React Data Gridhere
ref.current.elementref on DataGrid.Root
scrollToCell({ idx, rowIdx })gridRef.current?.engine.run("scroll-to-cell", { rowIndex, columnIndex, align }); align is "nearest", "start", "center" or "end", and either index alone scrolls one axis
setActivePosition({ idx, rowIdx })gridRef.current?.model.run("active-position.set", { rowIndex, columnIndex })
setActivePosition(position, { enableEditor: true })gridRef.current?.engine.run("edit-cell", { rowIndex, columnIndex })

Inside the root, useDataGrid() returns the same { model, engine }. See Model and engine and the scroll-to-cell example.

Column options

React Data Gridherenotes
keykeythe same
name (a string or an element)name (a string)the header's text; an element goes in renderHeaderCell
width (a number, "auto", "1fr", a percentage, "max-content")width (pixels, required), flex, autoSizeflex shares the room left in proportion, autoSize fits the content once, and the fit-columns action fits on demand. See Automatic widths
minWidth (default 50), maxWidthminWidth (default 40), maxWidththey limit a resizable column
frozen (true, "start", "end")pinned ("start" or "end")see Pinned columns
resizableresizablethe handle is your element, with useColumnResizer(cell)'s props
sortablesortablethe same
sortDescendingFirst—no direct equivalent: a column sorts ascending first. A middleware gives a column the other cycle (Descending first)
draggablereorderableon columns and on groups; the indicator is yours (data-drop-target)
renderCell(props)renderCell({ row, rowIndex, column, columnIndex, value })no onRowChange: update your rows. A cell's children replace it entirely
renderHeaderCell(props)renderHeaderCell({ column, columnIndex })headerCellContent(cell) is the default content, to place a resize handle beside
renderEditCell({ row, onRowChange, onClose })renderEditCell({ row, value, initialValue, startKey, onChange, onCommit, onCancel, editorProps })the draft is a value, not a row; editorProps marks a portalled popover as part of the edit
editable (a boolean, or a function of the row)editable (a boolean, or a function of the row and its index)there is no built-in text editor: give renderEditCell
editorOptions—a press outside the cell commits; the edit ends when its row or column leaves its place (by key); useCellEdit(cell) puts an editor inside a cell's own children, beside its content
colSpan({ type })colSpan({ type, rowIndex, … })type is lowercase: "header", "row" (with row), "summary", "group". See Column spanning
renderSummaryCell({ row })renderSummaryCell({ position, summaryIndex, column, columnIndex }), or DataGrid.SummaryCell childrenthere is no summary row object: your figures come from your component
renderGroupCell({ groupKey, childRows, isExpanded, toggleGroup })renderGroupCell({ group, rowIndex, column, columnIndex, value })the toggle is your control with useGroupToggle(row)
cellClass(row)className on DataGrid.Cell, a value or (state) => …the row is cell.row in the Cells children function
headerCellClassclassName on DataGrid.HeaderCellthe same for a group's header cell
summaryCellClassclassName on DataGrid.SummaryCell
—getValue, getCopyText, compare, filter, metaa cell's value, its copied text, the in-memory sort and filter, and anything of yours

Descending first

A click, Enter or Space on a sortable header cell runs sort-columns.toggle, which cycles ascending, descending, none. For the columns that should start descending, a middleware refuses the toggle and sets the sort it would give instead: the set runs right after the refused toggle, so a controlled Root is asked as for any sort.

descending-first.ts
import { type SortColumn, veto } from "@fragiola/data-grid-react";

const DESCENDING_FIRST = new Set(["budget", "updatedAt"]);

// descending, ascending, none: the reverse of the grid's own cycle
model.use((ctx, next) => {
    if (
        ctx.command !== "sort-columns.toggle" ||
        !DESCENDING_FIRST.has(ctx.payload.columnKey)
    ) {
        return next();
    }
    const { columnKey, multi = false } = ctx.payload;
    const sorts = ctx.state.sortColumns;
    const current = sorts.find((entry) => entry.columnKey === columnKey);
    const direction = !current
        ? "descending"
        : current.direction === "descending"
          ? "ascending"
          : null;
    // alone, the column replaces the sort; with Ctrl/⌘, it keeps its place among the others
    const others = multi
        ? sorts.filter((entry) => entry.columnKey !== columnKey)
        : [];
    const at = current && multi ? sorts.indexOf(current) : others.length;
    const sortColumns: SortColumn[] = direction
        ? [...others.slice(0, at), { columnKey, direction }, ...others.slice(at)]
        : others;
    // a dry run (`model.check`) changes nothing
    if (!ctx.dryRun) model.run("sort-columns.set", { sortColumns });
    return veto("replaced by sort-columns.set");
});

Column groups

A group is { key, name, children }, as in React Data Grid, with a key of its own (unique among the groups and columns). headerCellClass becomes className on its HeaderCell. A group can move whole (reorderable) and collapse (collapsible, its children's groupShow), with a label that stays in view as the columns scroll. See Column groups and Collapsible groups.

Grouping and trees

TreeDataGrid is no separate component here: one Root shows group rows and a tree's rows through the same row kinds (getRowMeta), and the grid is then a treegrid.

React Data Gridherenotes
TreeDataGridDataGrid.Root with useLocalRows's propsuseLocalRows(rows, columns, { groupBy }) from @fragiola/data-grid-react/local groups in memory and hands the root the rows shown
groupBygroupBy (an option of useLocalRows)the outer column first
rowGrouper(rows, columnKey)a column's getValuerows group by their value's text in that column; a server sends its own grouped rows (rowCount, getRow, getRowMeta)
expandedGroupIds, onExpandedGroupIdsChangeexpandedGroupKeys, onExpandedGroupKeysChange (on the hook, or on Root for a server's rows)groupKeyOf(path) makes a group's key
groupIdGetter—a group's key is its path (groupKeyOf)
a group's figures in renderGroupCellaggregates (an option of useLocalRows)your functions over a group's rows, by column key
a tree drawn with your own rows (the TreeView example)getSubRows (an option of useLocalRows)parents expand by their own key; a server can list children as they open

See Row grouping and Tree data.

Master-detail

React Data Grid's master-detail example adds detail rows to the rows, gives them a height with rowHeight and spans them with colSpan. Here a row expands into its own detail: DataGrid.RowDetail inside the row, the expanded rows' keys in expandedRowKeys (with onExpandedRowKeysChange or defaultExpandedRowKeys), and its height in detailHeight (a number, a function of the row, or "auto"). Indexes, counts and getRow stay those of your rows. See Master-detail.

Exports with no counterpart

React Data Gridhere
SelectColumn, SELECT_COLUMN_KEYa column of yours whose cells render a checkbox: model.is("row-selected", { rowIndex }), model.run("selected-rows.toggle", { rowIndex, extend })
useRowSelectionuseRow(row).state.selected, or the model's row-selected question, as above
useHeaderRowSelectionuseSelectAll(rowKeys) from @fragiola/data-grid-react/selection: status ("all", "some", "none"), toggle
renderTextEditoryour field in renderEditCell, from value and onChange
renderToggleGroup, ToggleGroupyour control with useGroupToggle(row)
renderHeaderCell, renderSortIcon, renderSortPriorityheaderCellContent(cell), and your own marks from data-sort and data-sort-priority
renderCheckbox, renderValue, SelectCellFormatteryour own components
Row, CellDataGrid.Row, DataGrid.Cell

What this grid adds

  • Rows by index. rowCount and getRow(index) stand for a dataset you do not hold: rows not loaded keep their room, and the windows tell you what to load. See Loading data.
  • Millions of rows and columns. Past the browser's size limit, scroll scaling maps the scrollbar onto the whole dataset, on both axes. See Scroll scaling.
  • Ranges of cells. Shift with the keys, a drag or Shift+click select a range, copied and pasted as tab-separated text. See Cell selection.
  • Rows as tall as their content. rowHeight="auto" measures each row as it renders. See Measured heights.
  • Collapsible column groups with labels that stay in view. See Collapsible groups.
  • Row reordering by the grid. onRowMove turns it on: a drag on your handle (useRowDragHandle) or Ctrl+Shift+↑ and ↓. See Row reordering.

The examples side by side

Each example of React Data Grid's website, and the one here that does the same:

React Data Gridhere
AllFeaturesall-features
Animationanimated-row-heights
CellNavigationcustom-navigation
ColumnGroupinggrouped-headers; its top and bottom summary rows in summary-rows
ColumnSpanningcolumn-spanning
ColumnsReorderingcolumn-reordering
CommonFeaturescommon-features
ContextMenucontext-menu
CustomizableRendererscustom-renderers
HeaderFiltersheader-filters
InfiniteScrollinginfinite-loading
MasterDetailmaster-detail
MillionCellslarge-dataset
NoRowsempty-state
ResizableGridresizable-grid
RowGroupingrow-grouping
RowsReorderingrow-reordering
ScrollToCellscroll-to-cell
TreeViewtree-data
VariableRowHeightvariable-row-height