-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture useGridColumns
📝 Generated from
docs/architecture/use-grid-columns.md. Edit it there; changes made in the wiki are overwritten.
Internal hook. Manages the full column lifecycle: injects hierarchy cell renderers, maintains column widths and order state, derives ordered/visible column lists, and computes the navigation column array used by keyboard navigation and spanning.
File: lib/hooks/core/useGridColumns.tsx (.tsx — contains JSX for hierarchy cell renderers)
Before extraction, ~195 lines of column management lived inline in DataGrid.tsx. Pulling them out gives this logic its own test surface and removes hierarchy rendering concerns from the main component.
The injected hierarchy renderers read hierarchy metadata from params.rowMeta (the GridRowMeta entry from rowMetaMap), never from the row object: rows are the consumer's own objects and carry no _treeDepth / _hasChildren fields (removed in v3.0). See GridRowMeta.
interface UseGridColumnsParams<R extends GridRowModel> {
activeColumns: GridColDef<R>[]; // pivot-resolved column list
isHierarchyEnabled: boolean; // true when treeData OR rowGrouping active
isRowGrouping: boolean;
isTreeData: boolean;
activeHierarchyHandlers: { toggleExpansion: (id: GridRowId) => void } | null;
columnVisibilityModel: Record<string, boolean>;
columnOrder?: string[]; // controlled; undefined = uncontrolled
onColumnOrderChange?: (params: GridColumnOrderChangeParams) => void;
onColumnOrderModelChange?: (columnOrder: string[]) => void; // whole new order; not in pivot mode
disableColumnReorder: boolean;
pivotMode: boolean; // pivot is active (DataGrid passes useGridPivot's isActive)
checkboxSelection: boolean;
hasDetailPanel: boolean;
rowReordering: boolean;
initialState?: GridInitialState;
setColumns: (cols: GridColDef[]) => void; // state store updater from useDataGrid
pinnedColumns?: GridColumnPinning;
getRowId?: (row: R) => GridRowId; // id passed to toggleExpansion; defaults to row.id
aggregationModel?: GridAggregationModel; // formats aggregates on row-grouping group rows
groupingRows?: ReadonlyMap<GridRowId, unknown>; // row grouping's synthetic group rows, by id
}
hasDetailPanelis resolved before the hook call — it depends only ongetDetailPanelContent(a prop):useGridDetailPanel, called just beforeuseGridColumnsinDataGrid.tsx, returns it asBoolean(getDetailPanelContent).
interface UseGridColumnsResult<R extends GridRowModel> {
effectiveColumns: GridColDef<R>[]; // hierarchy renderers injected
hierarchyField: string | undefined; // the column that gets the expand toggle / indent / group label
orderedColumns: GridColDef<R>[]; // sorted by effectiveColumnOrder
visibleOrderedColumns: GridColDef<R>[]; // filtered by columnVisibilityModel
navigationColumns: Array<GridColDef<R> | { field: string; sortable: false; editable: false }>; // system + visible data cols
columnIndexMap: Map<string, number>; // visible data column field -> public colIndex
columnWidths: Record<string, number>;
effectiveColumnOrder: string[];
setInternalColumnOrder: React.Dispatch<...>;
columnReorderHandlers: ReturnType<typeof useColumnReorder>; // header drag
moveColumn: (fromField: string, toField: string) => void; // Columns panel drag
resetColumnOrder: () => void; // Columns panel Reset
handleColumnResize: (field: string, newWidth: number) => void;
}When isHierarchyEnabled is false, effectiveColumns is activeColumns unchanged.
When true, the hierarchy column (hierarchyField) is the leftmost column actually on screen: the first visible column in render order (left-pinned in pinnedColumns.left order, then unpinned in column order, then right-pinned). Hiding, reordering or pinning columns therefore moves the toggle instead of dropping it. That column gets a renderCell override that:
- Reads
params.rowMeta(treeDepth,hasChildren,isExpanded,isGroupRow,groupLabel,descendantCount, …) - Renders indented padding (
treeDepth * 24px) - Shows an
<ExpandIcon>if the row has children; clicking it callstoggleExpansion(getRowId(row)) - Renders the content next to it: the consumer's
renderCelloutput if the column has one (isolated in its ownCellErrorBoundary, so a throw keeps the toggle), otherwise the default —params.formattedValuefor data rows, the group label plus(count)for row-grouping group rows and tree-data parents, nothing for subtotal (group footer) rows. For a synthetic row, a consumerrenderCellreturningundefinedfalls back to the default.
All other columns get a renderCell wrapper. Data rows use the column's own renderCell or params.formattedValue. On synthetic group rows the consumer's renderCell decides (undefined keeps the default); by default the grouping-field column is empty (the label already shows the value) and other columns show their value (the aggregate) or nothing.
Under row grouping, a column with an aggregationModel entry also gets valueGetter / valueFormatter wrappers for group rows (identified through groupingRows): the cell shows the aggregate stored on the group row (a valueGetter would recompute it from fields a group row does not have), formatted with formatAggregateForColumn, the formatter the footer, pivot cells and exports use (so count / unique are never put in the column's currency or unit format). Leaf rows keep the column's own getter and formatter.
-
Uncontrolled: the order is the columns' own order until the user reorders (
setInternalColumnOrder); it is derived, not snapshotted at mount, so it stays right when the columns change or the grid mounts in pivot mode. -
Controlled:
columnOrderwins, except in pivot mode. -
Every move works on the full current order,
orderedColumns.map(c => c.field): all current columns, including those added after mount, those missing from a stored or controlled order, and the synthetic__group__column. The header drag (columnReorderHandlers),moveColumnandresetColumnOrderall go through one commit step: it stores the new order (uncontrolled or pivot), firesonColumnOrderChangewith indices in that full order (not for Reset), and firesonColumnOrderModelChangewith the whole order (not in pivot mode). Before v3.0 the indices came fromorderedColumnsbut were applied toeffectiveColumnOrder, so a column added after the order was stored, or a partial controlled order, made a drag move the wrong column or nothing. -
Pivot mode: the generated pivot columns keep an order of their own, reset whenever the generated column set changes. Pivoting never rewrites the normal order, a controlled
columnOrder(which names source columns) does not apply, and reordering updates the pivot order.setInternalColumnOrderwrites to the order that is active.
visibleOrderedColumns drops columns hidden by columnVisibilityModel, except, in pivot mode, the pivot row-label columns (marked hideable: false): they share their field with the source column, which the visibility model may hide in normal mode, and the pivot rows are labelled by them.
navigationColumns is what Row and Header render, in render order: system column stubs, then the visible data columns sorted left-pinned, unpinned, right-pinned (the same order useLayout renders them in):
[{ field: '__reorder_col__', sortable: false, editable: false }] (if rowReordering)
[{ field: '__expand_col__', sortable: false, editable: false }] (if hasDetailPanel)
[{ field: '__checkbox_col__', sortable: false, editable: false }] (if checkboxSelection)
...visibleOrderedColumns in pinned render order
Hidden columns are not in it, so arrow keys never land on a column that is not rendered (v3.0; it used to be built from orderedColumns, in unpinned order). Its length is the grid's aria-colcount.
columnIndexMap maps each visible data column's field to its position in that render order, system columns excluded. Row, Cell and Header use it for the public colIndex (GridCellParams, renderCell, renderHeader) and for aria-colindex (colIndex + 1 + the number of system columns), so both are absolute and do not depend on the horizontal render window.
navigationColumns is consumed by useGridKeyboardNavigation (to map arrow-key movements across all focusable columns). useGridSpanning does not use it: spans are computed over the rendered data columns only (the layout's left-pinned, unpinned and right-pinned columns), so system columns and hidden columns are never part of a span.
DataGrid calls this hook after computing isHierarchyEnabled, activeHierarchyHandlers, and hoisting hasDetailPanel. The hook's return values are destructured directly into the variables the JSX return and downstream hooks expect.
OpenGridX 3.2.2 · MIT · This wiki is generated from docs/ on every push to main. To fix a page, open a PR against the source file.
Start here
Components
- DataGrid
- Header
- Row
- Cell
- Toolbar
- Pagination
- Filter Panel
- Tooltip
- Column Visibility
- Column Grouping
- Column Resizing
- Empty State
- Error Overlay
- Aggregation Footer
Features
- Virtualization
- Filtering & Search
- Sorting & Pagination
- Custom Pagination
- Editing & Reordering
- Row Selection
- Clipboard
- Pinning
- State Persistence
- Aggregation & Pivot
- Tree Data & Grouping
- Cell Spanning
- Master-Detail
- Keyboard & Accessibility
- List View
- Infinite Scroll
- Data Source
- Loading States
- Toolbar Customization
- Export (CSV, Excel, JSON, Print)
- PDF Export
Customization
Upgrading
Contributing
- Contributing
- Testing
- Roadmap
- DataGrid orchestration
- GridRowMeta
- useGridControlledState
- useGridRowPipeline
- useGridColumns
- useGridVirtualization
- useGridVisibleRows
- useGridScrollSync
- useGridStateSnapshot