@opencorestack/opengridx 2.1.0 โ†’ 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/CHANGELOG.md +399 -0
  2. package/README.md +95 -72
  3. package/dist/index.d.ts +590 -131
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7891 -5631
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +17 -23
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +348 -166
  10. package/docs/README.md +6 -1
  11. package/docs/architecture/datagrid-orchestration.md +81 -0
  12. package/docs/architecture/grid-row-meta.md +102 -0
  13. package/docs/architecture/use-grid-columns.md +128 -0
  14. package/docs/architecture/use-grid-controlled-state.md +157 -0
  15. package/docs/architecture/use-grid-row-pipeline.md +120 -0
  16. package/docs/architecture/use-grid-scroll-sync.md +111 -0
  17. package/docs/architecture/use-grid-state-snapshot.md +90 -0
  18. package/docs/architecture/use-grid-virtualization.md +115 -0
  19. package/docs/architecture/use-grid-visible-rows.md +85 -0
  20. package/docs/assets/banner-alt.png +0 -0
  21. package/docs/assets/banner.png +0 -0
  22. package/docs/assets/hero.png +0 -0
  23. package/docs/assets/logo.png +0 -0
  24. package/docs/components/aggregation-footer.md +15 -10
  25. package/docs/components/cell.md +16 -6
  26. package/docs/components/column-group-header.md +55 -28
  27. package/docs/components/column-resize.md +17 -13
  28. package/docs/components/column-visibility.md +6 -7
  29. package/docs/components/datagrid.md +15 -13
  30. package/docs/components/empty-state.md +2 -1
  31. package/docs/components/error-overlay.md +12 -7
  32. package/docs/components/filter-panel.md +28 -8
  33. package/docs/components/header.md +28 -9
  34. package/docs/components/pagination.md +14 -8
  35. package/docs/components/row.md +21 -5
  36. package/docs/components/toolbar.md +27 -9
  37. package/docs/components/tooltip.md +16 -7
  38. package/docs/customization/slots-api.md +90 -38
  39. package/docs/customization/theming.md +66 -18
  40. package/docs/features/aggregation-pivot.md +75 -9
  41. package/docs/features/cell-spanning.md +80 -125
  42. package/docs/features/clipboard.md +41 -16
  43. package/docs/features/custom-pagination.md +38 -18
  44. package/docs/features/data-source.md +36 -13
  45. package/docs/features/editing-reordering.md +125 -16
  46. package/docs/features/export-guide.md +126 -39
  47. package/docs/features/filtering.md +44 -6
  48. package/docs/features/infinite-scroll.md +54 -46
  49. package/docs/features/keyboard-navigation.md +83 -0
  50. package/docs/features/list-view.md +58 -5
  51. package/docs/features/loading-states.md +18 -20
  52. package/docs/features/master-detail.md +18 -5
  53. package/docs/features/pdf-export.md +72 -19
  54. package/docs/features/pinning.md +24 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +64 -14
  57. package/docs/features/state-persistence.md +78 -10
  58. package/docs/features/toolbar-customization.md +26 -17
  59. package/docs/features/tree-data-grouping.md +36 -8
  60. package/docs/features/virtualization.md +37 -14
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1233 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +5 -5
  65. package/lib/components/Cell/Cell.css +17 -1
  66. package/lib/components/Cell/Cell.tsx +129 -57
  67. package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
  68. package/lib/components/Cell/GridEditInputCell.tsx +114 -41
  69. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
  70. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +15 -2
  71. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
  72. package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
  73. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
  74. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
  75. package/lib/components/DataGrid/DataGrid.css +85 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +582 -876
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  79. package/lib/components/DataGrid/GridListView.tsx +165 -24
  80. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  81. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  82. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  83. package/lib/components/DataGrid/GridPinnedRows.tsx +62 -11
  84. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  85. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  86. package/lib/components/DataGrid/GridVirtualRows.tsx +40 -24
  87. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  88. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  89. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  90. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  91. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  92. package/lib/components/Header/ColumnMenu.tsx +91 -39
  93. package/lib/components/Header/Header.css +6 -3
  94. package/lib/components/Header/Header.tsx +181 -160
  95. package/lib/components/ListView/ListView.css +25 -0
  96. package/lib/components/ListView/ListViewRow.tsx +76 -10
  97. package/lib/components/Pagination/Pagination.tsx +15 -7
  98. package/lib/components/Row/Row.tsx +177 -77
  99. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  100. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  101. package/lib/components/Toolbar/GridToolbar.css +12 -11
  102. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  103. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  104. package/lib/components/Tooltip/Tooltip.css +20 -0
  105. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  106. package/lib/components/ui/Button.tsx +3 -0
  107. package/lib/components/ui/Checkbox.tsx +2 -1
  108. package/lib/components/ui/ExpandIcon.css +0 -9
  109. package/lib/components/ui/ExpandIcon.tsx +4 -1
  110. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  111. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  112. package/lib/hooks/core/useDataGrid.ts +115 -173
  113. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  114. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  115. package/lib/hooks/core/useGridApiRef.ts +19 -4
  116. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  117. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  118. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  119. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  120. package/lib/hooks/core/useGridColumns.tsx +279 -76
  121. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  122. package/lib/hooks/core/useGridControlledState.ts +99 -6
  123. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  124. package/lib/hooks/core/useGridDevWarnings.ts +104 -5
  125. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  126. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  127. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  128. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  129. package/lib/hooks/core/useGridKeyboardNavigation.ts +447 -243
  130. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  131. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  132. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  133. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  134. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  135. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  136. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  137. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  138. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  139. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  140. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  141. package/lib/hooks/core/useGridViewport.ts +38 -0
  142. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  143. package/lib/hooks/core/useGridVisibleRows.ts +17 -5
  144. package/lib/hooks/core/useLayout.ts +215 -195
  145. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  146. package/lib/hooks/core/useStableColumns.ts +34 -0
  147. package/lib/hooks/features/useAggregation.ts +82 -51
  148. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  149. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  150. package/lib/hooks/features/useGridClipboard.ts +133 -73
  151. package/lib/hooks/features/useGridDataSource.ts +354 -128
  152. package/lib/hooks/features/useGridEditing.ts +211 -51
  153. package/lib/hooks/features/useGridPivot.ts +92 -0
  154. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  155. package/lib/hooks/features/useGridSpanning.ts +410 -306
  156. package/lib/hooks/features/usePivot.ts +0 -0
  157. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  158. package/lib/hooks/useColumnReorder.ts +38 -44
  159. package/lib/hooks/useRowGrouping.ts +298 -160
  160. package/lib/hooks/useRowReorder.ts +62 -31
  161. package/lib/hooks/useTreeData.ts +287 -220
  162. package/lib/index.ts +28 -2
  163. package/lib/state/index.ts +1 -0
  164. package/lib/state/types.ts +7 -1
  165. package/lib/state/useGridStateStorage.ts +88 -29
  166. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  167. package/lib/theme/gridThemeContext.ts +5 -0
  168. package/lib/theme/index.ts +5 -0
  169. package/lib/theme/presets.ts +5 -4
  170. package/lib/theme/types.ts +40 -2
  171. package/lib/types/index.ts +313 -116
  172. package/lib/utils/aggregation/index.ts +119 -24
  173. package/lib/utils/aria/index.ts +40 -0
  174. package/lib/utils/columnGroups.ts +110 -0
  175. package/lib/utils/columnLookup.ts +33 -0
  176. package/lib/utils/columnWidth.ts +22 -0
  177. package/lib/utils/dataSource/index.ts +46 -0
  178. package/lib/utils/detailPanel.ts +66 -0
  179. package/lib/utils/dragData.ts +44 -0
  180. package/lib/utils/editing/index.ts +30 -0
  181. package/lib/utils/export/exportShared.ts +197 -0
  182. package/lib/utils/export/exportToExcelAdvanced.ts +278 -139
  183. package/lib/utils/export/exportToPdf.ts +143 -92
  184. package/lib/utils/export/groupLabel.ts +14 -0
  185. package/lib/utils/export/index.ts +278 -272
  186. package/lib/utils/export/pdfText.ts +50 -0
  187. package/lib/utils/filtering/index.ts +277 -148
  188. package/lib/utils/focus/index.ts +115 -0
  189. package/lib/utils/gridApiRows.ts +35 -0
  190. package/lib/utils/gridModes.ts +77 -0
  191. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  192. package/lib/utils/pagination/index.ts +56 -0
  193. package/lib/utils/pinning/index.ts +77 -33
  194. package/lib/utils/pivot/index.ts +368 -0
  195. package/lib/utils/scroll/index.ts +55 -5
  196. package/lib/utils/scroll/scrollEnd.ts +18 -0
  197. package/lib/utils/sorting/index.ts +154 -30
  198. package/lib/utils/syntheticRows.ts +12 -0
  199. package/lib/utils/values.ts +206 -0
  200. package/lib/utils/viewport.ts +14 -0
  201. package/llms.txt +261 -164
  202. package/package.json +13 -5
  203. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  204. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  205. package/lib/components/DataGrid/DataGrid.defects.test.tsx +0 -103
  206. package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
  207. package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
  208. package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
  209. package/lib/components/DataGrid/DataGrid.virtualization.browser.test.tsx +0 -183
  210. package/lib/components/Header/Header.test.tsx +0 -135
  211. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  212. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  213. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  214. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  215. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  216. package/lib/hooks/features/useAggregation.test.ts +0 -175
  217. package/lib/hooks/features/usePivot.test.ts +0 -202
  218. package/lib/hooks/useRowGrouping.test.ts +0 -298
  219. package/lib/hooks/useTreeData.test.ts +0 -83
  220. package/lib/utils/aggregation/aggregation.test.ts +0 -16
  221. package/lib/utils/export/exportToExcelAdvanced.test.ts +0 -109
  222. package/lib/utils/export/exportToPdf.test.ts +0 -120
  223. package/lib/utils/export/groupedExport.test.ts +0 -144
  224. package/lib/utils/filtering/filtering.test.ts +0 -178
  225. package/lib/utils/sorting/sorting.test.ts +0 -88
@@ -10,18 +10,29 @@ Generate a styled PDF report from grid data using `exportToPdf()`.
10
10
  npm install jspdf jspdf-autotable
11
11
  ```
12
12
 
13
+ > **Currency symbols such as `โ‚น` print as `?` by default.** The built-in Helvetica font only covers Latin-1. Pass the [`font` option](#non-latin-text-and-unicode-fonts) with a Unicode `.ttf`:
14
+ >
15
+ > ```ts
16
+ > await exportToPdf(rows, columns, { fileName: 'invoices', font: { name: 'NotoSans', data: notoSansBase64 } });
17
+ > ```
18
+
19
+ > **Large datasets:** PDF generation runs on the main thread, and time and file size grow with every row: 50,000 grouped rows took 10โ€“16 s (the page is unresponsive meanwhile) and produced a file of about 120 MB. For more than a few thousand rows, export CSV (`exportToCsv`) or `.xlsx` (`exportToExcelAdvanced`) instead, or export a filtered subset.
20
+
13
21
  ## Basic Usage
14
22
 
15
23
  ```tsx
16
- import { exportToPdf, useGridApiRef } from '@opencorestack/opengridx';
24
+ import {
25
+ DataGrid, GridToolbar, exportToPdf, useGridApiRef,
26
+ type GridColDef, type GridRowModel,
27
+ } from '@opencorestack/opengridx';
17
28
 
18
- function MyGrid() {
29
+ function MyGrid({ rows, columns }: { rows: GridRowModel[]; columns: GridColDef[] }) {
19
30
  const apiRef = useGridApiRef();
20
31
 
21
32
  const handleExport = async () => {
22
33
  await exportToPdf(
23
- apiRef.current.getVisibleRows(),
24
- apiRef.current.getVisibleColumns(),
34
+ apiRef.current.getAllFilteredRows(), // every row passing the filter, all pages
35
+ apiRef.current.getVisibleColumns(), // on-screen columns, in display order
25
36
  { fileName: 'my-report', title: 'Sales Report' }
26
37
  );
27
38
  };
@@ -31,14 +42,13 @@ function MyGrid() {
31
42
  apiRef={apiRef}
32
43
  rows={rows}
33
44
  columns={columns}
34
- slots={{
35
- toolbar: () => (
36
- <GridToolbar
37
- renderExportButton={() => (
38
- <button onClick={handleExport}>Export PDF</button>
39
- )}
40
- />
41
- ),
45
+ slots={{ toolbar: GridToolbar }}
46
+ slotProps={{
47
+ toolbar: {
48
+ renderExportButton: () => (
49
+ <button onClick={handleExport}>Export PDF</button>
50
+ ),
51
+ },
42
52
  }}
43
53
  />
44
54
  );
@@ -49,7 +59,7 @@ function MyGrid() {
49
59
 
50
60
  ```tsx
51
61
  await exportToPdf(
52
- apiRef.current.getVisibleRows(),
62
+ apiRef.current.getAllFilteredRows(),
53
63
  apiRef.current.getVisibleColumns(),
54
64
  {
55
65
  fileName: 'employee-report',
@@ -76,14 +86,49 @@ await exportToPdf(
76
86
  | `title` | `string` | โ€” | Adds a branded header block above the table |
77
87
  | `logoUrl` | `string` | โ€” | Data URI or URL for a logo image (requires `title`) |
78
88
  | `orientation` | `'portrait' \| 'landscape'` | `'landscape'` | Page orientation |
79
- | `selectedRows` | `(string \| number)[]` | โ€” | Export only rows with these IDs |
89
+ | `selectedRows` | `(string \| number)[]` | โ€” | Export only rows with these IDs. Takes precedence over `groupedRows`; the footer totals are recomputed over the selected rows (v3.0+) |
90
+ | `getRowId` | `(row) => GridRowId` | `row.id` | The grid's `getRowId`, when it has one, so `selectedRows` match rows that keep their id in another field (v3.0+) |
80
91
  | `aggregationResult` | `Record<string, unknown>` | โ€” | From `apiRef.current.getAggregationResult()` |
81
92
  | `aggregationModel` | `GridAggregationModel` | โ€” | From `apiRef.current.getAggregationModel()` |
82
- | `filterModel` | `GridFilterModel` | โ€” | From `apiRef.current.getFilterModel()` |
93
+ | `filterModel` | `GridFilterModel` | โ€” | From `apiRef.current.getFilterModel()`. Summarised under the title (see below) |
94
+ | `groupedRows` | `GridGroupedExportRow[]` | โ€” | From `apiRef.current.getGroupedExportRows()`: group headers, subtotals and a grand-total footer |
83
95
  | `alternateRowColor` | `boolean` | `true` | Alternating row background shading |
84
- | `headerBackgroundColor` | `string` | `'#4f46e5'` | Column header cell background (hex) |
85
- | `headerTextColor` | `string` | `'#ffffff'` | Column header cell text color (hex) |
96
+ | `headerBackgroundColor` | `string` | `'#4f46e5'` | Column header cell background (`#rrggbb` or `#rgb`). Invalid values fall back to the default |
97
+ | `headerTextColor` | `string` | `'#ffffff'` | Column header cell text color (`#rrggbb` or `#rgb`). Invalid values fall back to the default |
86
98
  | `fontSize` | `number` | `9` | Body cell font size in points |
99
+ | `font` | `{ name: string; data: string; boldData?: string }` | โ€” | A TrueType font (base64 `.ttf`) for text outside Latin-1 (v3.0+). See [Non-Latin text](#non-latin-text-and-unicode-fonts) |
100
+
101
+ ## Totals and filter summary
102
+
103
+ - The footer shows `TOTAL` in the first column; when the first column has its own aggregate, the value is kept next to it (`TOTAL: 30`). Grouped `Subtotal` and `Grand Total` rows do the same.
104
+ - `count` and `unique` are plain counts, not run through the column's `valueFormatter`. `sum`, `avg`, `min` and `max` use the formatter; if it throws (e.g. it reads a data-row field), the value is formatted like the grid footer instead of aborting the export. See [Aggregate values](export-guide.md#aggregate-values).
105
+ - The filter line under the title lists the applied filter as the grid evaluates it: `A equals "1" OR (B contains "x" AND C isEmpty) โ€ข Search: "acme"` โ€” nested groups in parentheses, the model's logic operator, and the quick-search terms. Rules without a value (an unfinished row in the filter panel) are left out. Long titles and filter lines wrap within the page margins.
106
+
107
+ ## Non-latin text and Unicode fonts
108
+
109
+ jsPDF's built-in Helvetica font only covers Latin-1 and the WinAnsi punctuation (`รฉ`, `รผ`, `โ‚ฌ`, `โ€œ โ€`, `โ€ข`). A string with any other character used to come out entirely garbled; since v3.0 only the undrawable characters are replaced with `?` (special spaces and the minus sign become their ASCII forms, so `fr-FR` totals such as `1 234 567` print correctly) and a console warning is logged.
110
+
111
+ To print `โ‚น`, Greek, Cyrillic, CJK or Devanagari text, pass a Unicode TrueType font such as [Noto Sans](https://fonts.google.com/noto) as base64:
112
+
113
+ ```tsx
114
+ const toBase64 = async (url: string) => {
115
+ const bytes = new Uint8Array(await (await fetch(url)).arrayBuffer());
116
+ let binary = '';
117
+ bytes.forEach(b => { binary += String.fromCharCode(b); });
118
+ return btoa(binary);
119
+ };
120
+
121
+ await exportToPdf(rows, columns, {
122
+ title: 'เคฌเคฟเค•เฅเคฐเฅ€ เคฐเคฟเคชเฅ‹เคฐเฅเคŸ',
123
+ font: {
124
+ name: 'NotoSans',
125
+ data: await toBase64('/fonts/NotoSans-Regular.ttf'),
126
+ boldData: await toBase64('/fonts/NotoSans-Bold.ttf'), // optional; the regular face is used for bold otherwise
127
+ },
128
+ });
129
+ ```
130
+
131
+ jsPDF does not shape complex scripts, so Devanagari or Arabic conjuncts may not join.
87
132
 
88
133
  ## Column Exclusion
89
134
 
@@ -93,10 +138,18 @@ Columns with `exportable: false` are excluded automatically:
93
138
  { field: 'internalNotes', headerName: 'Notes', exportable: false }
94
139
  ```
95
140
 
96
- System columns (`__check__`, `__actions__`) are also always excluded.
141
+ System columns (`__check__`, `__actions__`) and spacer columns (`isSpacer: true`) are also always excluded.
97
142
 
98
143
  ## Troubleshooting
99
144
 
145
+ **`โ‚น` or other characters print as `?`**
146
+
147
+ Pass the `font` option with a Unicode TrueType font. See [Non-Latin text](#non-latin-text-and-unicode-fonts).
148
+
149
+ **Export is slow or the file is huge**
150
+
151
+ PDF is not suited to tens of thousands of rows (see Installation above). Use CSV or `.xlsx` for large exports.
152
+
100
153
  **Error: "exportToPdf requires 'jspdf' and 'jspdf-autotable'"**
101
154
 
102
155
  Run: `npm install jspdf jspdf-autotable`
@@ -107,4 +160,4 @@ Ensure `logoUrl` is a data URI (base64-encoded) or an absolute URL that is CORS-
107
160
 
108
161
  ## Exporting unfiltered data
109
162
 
110
- `getVisibleRows()` returns only rows that pass the current filter. Use `getAllRows()` if you want to export all data regardless of active filters.
163
+ `getVisibleRows()` returns the rows on screen: those that pass the current filter, only the current page when pagination is on, plus pinned rows. `getAllFilteredRows()` returns every filtered row regardless of pagination (and, under grouping or tree data, of expansion), in the grid's sort order with top- and bottom-pinned rows first and last; it is the right source for a full export. Use `getAllRows()` if you want to export all data regardless of active filters. `getVisibleColumns()` returns the columns on screen (hidden columns omitted), in display order. `getFilterModel()` returns the live filter model, including filters applied from the toolbar or filter panel.
@@ -18,12 +18,21 @@ Columns can be pinned to the left or right edges of the grid. Pinned columns rem
18
18
  />
19
19
  ```
20
20
 
21
+ ### Order and widths
22
+ - Pinned columns render in the order of `pinnedColumns.left` / `pinnedColumns.right`, not in column order. Pinning from the column menu appends the column to the end of its side, so it lands next to the scrolling columns.
23
+ - When the columns are narrower than the grid, right-pinned columns sit at the grid's right edge, and the free space is between the scrolling columns and them (v3.0.1+).
24
+ - A pinned column is sized exactly as it would be unpinned: numbers, percentages, `flex` and `'auto'` all apply, clamped to `minWidth` / `maxWidth`. The sticky offsets use those widths, so pinned columns never overlap.
25
+
21
26
  ### Column Configuration
22
- Individual columns can be marked as non-pinnable:
27
+ Individual columns can be marked as non-pinnable. Their column menu has no pin actions:
23
28
  ```typescript
24
29
  { field: 'id', pinnable: false }
25
30
  ```
26
31
 
32
+ `pinnable: false` only affects the UI; a column listed in `pinnedColumns` is still pinned. It does not block drag-reordering (v3.0+).
33
+
34
+ Pinned column headers are not draggable and are not drop targets for header drag-reorder: to move a pinned column, change the order in `pinnedColumns`.
35
+
27
36
  ---
28
37
 
29
38
  ## โฌ†๏ธ Row Pinning
@@ -40,8 +49,13 @@ Rows can be pinned to the top or bottom of the grid. Pinned rows remain visible
40
49
  />
41
50
  ```
42
51
 
52
+ ### Behaviour
53
+ - Pinned rows are split out after filtering: a pinned row that does not match the active filter or quick filter is hidden. They are never sorted; they keep the order of `pinnedRows.top` / `.bottom`.
54
+ - `pinnedRows` is **ignored while `treeData` or `rowGroupingModel` is active**: the rows stay in their place in the hierarchy, and a development warning is logged.
55
+ - Top-pinned rows stick directly below the whole header, including column group header rows. Bottom-pinned rows stick directly above the aggregation footer; both stay visible.
56
+
43
57
  ### Use Cases
44
- - **Header Summary**: Keep a "Totals" or "Averages" row at the top.
58
+ - **Header Summary**: Keep a summary row at the top (for grid-wide totals that follow the filter, prefer `aggregationModel`, whose footer never scrolls away).
45
59
  - **Comparison**: Pin a specific row to compare it against other data.
46
60
  - **Actions footer**: Pin a specialized row for bulk actions at the bottom.
47
61
 
@@ -59,8 +73,13 @@ Rows can be pinned to the top or bottom of the grid. Pinned rows remain visible
59
73
  ---
60
74
 
61
75
  ## ๐ŸŽจ Styling Pinning
62
- Pinned elements have specific CSS shadow classes applied to indicate depth:
63
- - `.ogx-column--pinned-left--last`
64
- - `.ogx-column--pinned-right--first`
76
+ The last left-pinned cell and the first right-pinned cell of each row carry section-edge classes (the body cells draw an edge shadow):
77
+ - body cells: `.ogx__cell--pinned-left-last`, `.ogx__cell--pinned-right-first`
78
+ - header cells: `.ogx__header-cell--pinned-left-last`, `.ogx__header-cell--pinned-right-first`
79
+ - aggregation footer: `.ogx__aggregation-cell--pinned-left-last`, `.ogx__aggregation-cell--pinned-right-first`
80
+
81
+ The edge is the last / first pinned column that is actually shown, so hiding a pinned column moves it. All pinned cells also carry `.ogx__cell--pinned-left` / `--pinned-right` (`.ogx__header-cell--pinned-left` / `--pinned-right`).
82
+
83
+ Sticky row areas are wrapped (v3.0+): the header and the top-pinned rows sit in `div.ogx__sticky-top`, and the bottom-pinned rows and the aggregation footer sit in `div.ogx__sticky-bottom`. The `.ogx__pinned-rows--top` / `.ogx__pinned-rows--bottom` containers inside them are no longer sticky themselves, so custom sticky CSS belongs on the wrappers.
65
84
 
66
85
  These can be customized via your theme or global CSS.
@@ -19,6 +19,8 @@ OpenGridX provides various ways for users to select one or multiple rows.
19
19
  ### Enable Checkbox Selection
20
20
  ```tsx
21
21
  <DataGrid
22
+ rows={rows}
23
+ columns={columns}
22
24
  checkboxSelection
23
25
  pinCheckboxColumn // Optional: keeps checkbox on the left during horizontal scroll
24
26
  />
@@ -31,6 +33,8 @@ Use the `rowSelectionModel` to control the selection state from your parent comp
31
33
  const [selection, setSelection] = useState<GridRowId[]>([]);
32
34
 
33
35
  <DataGrid
36
+ rows={rows}
37
+ columns={columns}
34
38
  rowSelectionModel={selection}
35
39
  onRowSelectionModelChange={(newSelection) => setSelection(newSelection)}
36
40
  />
@@ -42,6 +46,8 @@ Use `disableRowSelectionOnClick` to prevent clicking a row from changing the sel
42
46
 
43
47
  ```tsx
44
48
  <DataGrid
49
+ rows={rows}
50
+ columns={columns}
45
51
  checkboxSelection
46
52
  disableRowSelectionOnClick
47
53
  />
@@ -49,15 +55,41 @@ Use `disableRowSelectionOnClick` to prevent clicking a row from changing the sel
49
55
 
50
56
  ### Single-Row Selection Only
51
57
 
52
- Use `disableMultipleRowSelection` to cap selection to one row at a time. Clicking a second row deselects the first; clicking an already-selected row deselects it.
58
+ Use `disableMultipleRowSelection` to cap selection to one row at a time. Clicking a second row deselects the first; clicking an already-selected row deselects it. The cap applies to row clicks, row checkboxes, the Space key and `apiRef.selectRow` / `selectRows` alike, and the header select-all checkbox is not shown.
53
59
 
54
60
  ```tsx
55
61
  <DataGrid
62
+ rows={rows}
63
+ columns={columns}
56
64
  disableMultipleRowSelection
57
65
  onRowSelectionModelChange={(model) => console.log('selected:', model)}
58
66
  />
59
67
  ```
60
68
 
69
+ ### Select All
70
+
71
+ With `checkboxSelection`, the header checkbox selects the rows that pass the current filter (including pinned rows) and adds them to the selection; unchecking it removes those rows again. Rows hidden by the filter keep their selection state, and under row grouping group rows are not part of select-all. Synthetic rows (row-grouping group and subtotal rows, auto-created tree-data parents, the pivot Grand Total) have no checkbox and are never selected, by click, checkbox, Space key or `apiRef.selectRow` / `selectRows` (v3.0+ for the Grand Total and `apiRef`); tree-data parents that are your own rows are selectable like any row. A click on the Grand Total still fires `onRowClick`. `apiRef.selectRow` on a row that is already selected reports nothing, as a click on the checkbox would not. The header shows checked when every such row is selected and indeterminate when only some are, so ids of rows that are no longer in `rows` do not affect it.
72
+
73
+ ### Removed Rows
74
+
75
+ When rows leave `rows`, their ids leave the selection (v3.0+): `apiRef.current.getSelectedRows()`, the header checkbox and the next model a click produces no longer contain them, and the pruned model is reported once through `onRowSelectionModelChange`. An uncontrolled selection forgets the ids, so a row that comes back is not selected again; a controlled model should adopt the reported value. This applies when the grid holds every row: without a `dataSource`, with client pagination and filtering, and outside pivot mode. With `paginationMode="server"`, `filterMode="server"` or a `dataSource`, ids of rows that are not loaded are kept, so a selection survives paging.
76
+
77
+ In pivot mode the pivot rows have their own ids (`'__pivot_row__:["North"]'`, derived from the row-field values), so a selection made on the source rows never marks a pivot row, and leaving pivot mode gives the source selection back.
78
+
79
+ ### Keyboard Selection
80
+
81
+ With focus in the grid (see [Keyboard & Accessibility](keyboard-navigation.md)):
82
+
83
+ | Key | Action |
84
+ | :--- | :--- |
85
+ | **Shift+Space** | Select or deselect the focused row. Available when rows can be selected at all (`checkboxSelection`, or click selection not disabled with `disableRowSelectionOnClick`). |
86
+ | **Space** / **Enter** on a row checkbox | Toggle that row. |
87
+ | **Space** / **Enter** on the select-all header | Select or clear all rows, like clicking it. |
88
+ | **Enter** on a non-editable cell | Same as clicking the row: `onRowClick`, then click-to-select unless `disableRowSelectionOnClick`. |
89
+ | **Ctrl+A** / **Cmd+A** | Select every row, unless `disableMultipleRowSelection` (or selection is not possible). |
90
+
91
+ Synthetic rows (row-grouping groups and subtotals, auto-created tree parents, the pivot Grand Total) are never selected from the keyboard. The grid sets `aria-multiselectable="true"` when several rows can be selected.
92
+
61
93
  ---
62
94
 
63
95
  ## โš™๏ธ API Reference
@@ -77,17 +109,25 @@ Use `disableMultipleRowSelection` to cap selection to one row at a time. Clickin
77
109
  ## ๐Ÿ–ฑ๏ธ Interaction Callbacks
78
110
 
79
111
  ### `onRowClick`
80
- Fired when a row is clicked (even if selection is disabled on click).
81
- ```typescript
82
- onRowClick: (params: GridRowParams) => {
83
- console.log('Row clicked:', params.id, params.row);
84
- }
112
+ Fired when a row is clicked, or Enter is pressed on one of its non-editable cells, even if selection is disabled on click. It is not fired for synthetic row-grouping group rows (a click toggles them instead).
113
+ ```tsx
114
+ <DataGrid
115
+ rows={rows}
116
+ columns={columns}
117
+ onRowClick={(params: GridRowParams) => {
118
+ console.log('Row clicked:', params.id, params.row);
119
+ }}
120
+ />
85
121
  ```
86
122
 
87
123
  ### `onCellClick`
88
124
  Fired when a specific cell is clicked.
89
- ```typescript
90
- onCellClick: (params: GridCellParams) => {
91
- console.log('Cell clicked:', params.field, params.value);
92
- }
125
+ ```tsx
126
+ <DataGrid
127
+ rows={rows}
128
+ columns={columns}
129
+ onCellClick={(params: GridCellParams) => {
130
+ console.log('Cell clicked:', params.field, params.value);
131
+ }}
132
+ />
93
133
  ```
@@ -1,6 +1,6 @@
1
1
  # ๐Ÿ”ข Sorting & Pagination
2
2
 
3
- Efficiently naviagte and organize through large datasets using OpenGridX's sorting and pagination engines.
3
+ Efficiently navigate and organize through large datasets using OpenGridX's sorting and pagination engines.
4
4
 
5
5
  ---
6
6
 
@@ -34,18 +34,32 @@ OpenGridX supports two modes of multi-column sorting:
34
34
 
35
35
  Each active sort column shows a numbered priority badge (`1`, `2`, `3`โ€ฆ) next to its arrow.
36
36
 
37
- - Clicking (or shift-clicking) an already-sorted column cycles its direction: `asc โ†’ desc โ†’ removed`.
37
+ - Clicking (or shift-clicking) an already-sorted column cycles its direction: `asc โ†’ desc โ†’ removed`. The column keeps its priority while its direction changes; it is not moved to the end of the sort model.
38
38
  - A plain click *without* `multiSort` and without Shift always resets to a single-key sort on that column.
39
+ - The column menu acts on that column only: **Unsort** removes just that key, and **Sort Ascending / Descending** on a column that is already sorted changes its direction in place. On a column that is not sorted yet it appends with `multiSort` and replaces the model without it, like a plain click.
39
40
  - Set `sortable: false` on a column to exclude it entirely.
40
41
 
42
+ ### How values are compared (client-side)
43
+
44
+ - Cells are read through the column's `valueGetter`, so computed columns sort by the value they display.
45
+ - `type: 'number'` columns compare numerically even when the data holds numeric strings (`'9'` sorts before `'10'`). `type: 'date'` columns parse date strings (`'YYYY-MM-DD'` is read as a local date) and compare chronologically.
46
+ - Strings use language-aware ordering (`Intl.Collator`): case is ignored, accented letters sort next to their base letter (`'ร‰mile'` between `'Adam'` and `'Zoe'`), and digit runs compare by value (`'item9'` before `'item10'`).
47
+ - `null`, `undefined`, `NaN`, an Invalid Date and, in number / date columns, values that cannot be parsed are "empty": they sort last in ascending order and first in descending order.
48
+ - A column that mixes kinds of value (numbers and strings, say) is ordered by kind first โ€” numbers, dates, booleans, strings โ€” so the order does not depend on the input order.
49
+ - Ties keep the rows' original order.
50
+
41
51
  ```tsx
42
52
  // Controlled multi-sort โ€” set programmatically or drive from UI with multiSort:
53
+ const [sortModel, setSortModel] = useState<GridSortItem[]>([
54
+ { field: 'department', sort: 'asc' },
55
+ { field: 'salary', sort: 'desc' },
56
+ ]);
57
+
43
58
  <DataGrid
59
+ rows={rows}
60
+ columns={columns}
44
61
  multiSort
45
- sortModel={[
46
- { field: 'department', sort: 'asc' },
47
- { field: 'salary', sort: 'desc' },
48
- ]}
62
+ sortModel={sortModel}
49
63
  onSortModelChange={setSortModel}
50
64
  />
51
65
  ```
@@ -66,25 +80,60 @@ OpenGridX supports standard page-based pagination and infinite scrolling.
66
80
 
67
81
  ### Usage
68
82
  ```tsx
83
+ // Uncontrolled: the grid keeps the page; initialState sets where it starts
69
84
  <DataGrid
85
+ rows={rows}
86
+ columns={columns}
70
87
  pagination
71
- paginationModel={{ page: 0, pageSize: 10 }}
72
88
  pageSizeOptions={[5, 10, 20, 50]}
89
+ initialState={{ pagination: { paginationModel: { page: 0, pageSize: 10 } } }}
90
+ />
91
+
92
+ // Controlled: pass onPaginationModelChange too, or the pager cannot change page
93
+ const [paginationModel, setPaginationModel] = useState({ page: 0, pageSize: 10 });
94
+ <DataGrid
95
+ rows={rows}
96
+ columns={columns}
97
+ pagination
98
+ paginationModel={paginationModel}
99
+ onPaginationModelChange={setPaginationModel}
73
100
  />
74
101
  ```
75
102
 
103
+ The pager counts the rows being paged: the rows that pass the filter, excluding pinned rows (pinned rows show on every page).
104
+
105
+ Without `paginationModel` or `initialState.pagination`, the page size is 100 when `pageSizeOptions` offers it and otherwise the first option, so the rows-per-page select always matches the rows shown.
106
+
107
+ ### When the rows shrink
108
+ If the current page ends up past the last page (new `rows`, a filter, collapsed tree nodes), the grid shows the last page and reports the corrected model through `onPaginationModelChange`, so Previous and Next continue from the page on screen. The correction waits until rows are present and nothing is loading, so a restored page (for example from `initialState.pagination`) survives an initially empty `rows` array. Under server pagination the server's `rowCount` decides; the grid does not clamp.
109
+
110
+ ### Page sizes outside `pageSizeOptions`
111
+ When the page size in use is not one of `pageSizeOptions` (for example a controlled `paginationModel={{ page: 0, pageSize: 20 }}` with `pageSizeOptions={[10, 25, 50]}`), the rows-per-page select adds it as an extra option, so it shows the size actually used. A page size below 1 is treated as 1.
112
+
76
113
  ### Server-Side Pagination
77
- When using `paginationMode="server"`, you must provide the `rowCount` and handle page changes in your `dataSource`.
114
+ With `paginationMode="server"` the grid does not slice rows: `rows` is the current page. There are two ways to supply it.
115
+
116
+ **Fetch the page yourself** โ€” control `paginationModel`, pass the page's rows and the server total as `rowCount`. `rowCount` is read on every render, so update it when the total changes.
78
117
 
79
118
  ```tsx
119
+ const [paginationModel, setPaginationModel] = useState({ page: 0, pageSize: 10 });
120
+ const { rows, total } = usePageQuery(paginationModel); // your data fetching
121
+
80
122
  <DataGrid
123
+ rows={rows} // only the current page
124
+ columns={columns}
81
125
  pagination
82
126
  paginationMode="server"
83
- rowCount={1000} // Total rows on server
84
- onPaginationModelChange={(model) => fetchNextPage(model)}
127
+ rowCount={total} // total rows on the server
128
+ paginationModel={paginationModel}
129
+ onPaginationModelChange={setPaginationModel}
85
130
  />
86
131
  ```
87
132
 
133
+ **Or use a `dataSource`** โ€” the grid calls `getRows` with `startRow` / `endRow` and uses the response's `rowCount` as the total (the `rowCount` prop is only a fallback until the first response). See [Server-Side Data](./data-source.md).
134
+
135
+ `sortingMode="server"` and `filterMode="server"` work the same way: the grid fires `onSortModelChange` / `onFilterModelChange` and shows `rows` as given, without re-sorting or re-filtering them, with or without a `dataSource`.
136
+
88
137
  ### Props
89
138
  | Prop | Type | Default | Description |
90
139
  | :--- | :--- | :--- | :--- |
@@ -92,14 +141,15 @@ When using `paginationMode="server"`, you must provide the `rowCount` and handle
92
141
  | `paginationMode` | `'client' \| 'server' \| 'infinite'` | `'client'` | Location of the pagination logic. |
93
142
  | `paginationModel` | `GridPaginationModel` | โ€” | Controlled pagination state (`{ page, pageSize }`). |
94
143
  | `onPaginationModelChange` | `(model: GridPaginationModel) => void` | โ€” | Fired when page or page size changes. |
95
- | `pageSizeOptions` | `number[]` | `[10, 25, 50, 100]` | Options for the rows-per-page selector. |
144
+ | `pageSizeOptions` | `number[]` | `[10, 25, 50, 100]` | Options for the rows-per-page selector. The uncontrolled default page size is 100 when offered, else the first option. |
145
+ | `rowCount` | `number` | โ€” | Server total for `paginationMode="server"` without a `dataSource` (with one, the response's `rowCount` is used). |
96
146
 
97
147
  ---
98
148
 
99
149
  ## โšก Infinite Scrolling
100
150
  For a more modern experience, use infinite scroll combined with virtualization.
101
151
 
102
- 1. Set `paginationMode="infinite"`.
103
- 2. Implement `onRowsScrollEnd` or use the `dataSource.getRows` method to fetch data as the user scrolls.
152
+ 1. Set `paginationMode="infinite"` and pass a `dataSource`.
153
+ 2. Advance `paginationModel.page` from `onRowsScrollEnd`; the grid requests the next rows from `dataSource.getRows` and appends them. The pager is not shown and rows are not sliced.
104
154
 
105
- ๐Ÿ“– **[Full Infinite Scroll Guide](docs/features/infinite-scroll.md)**
155
+ ๐Ÿ“– **[Full Infinite Scroll Guide](./infinite-scroll.md)**
@@ -6,18 +6,29 @@ Save and restore the grid's configuration (sorting, filtering, column order, etc
6
6
 
7
7
  ## ๐Ÿ—๏ธ The `initialState` Prop
8
8
 
9
- You can pre-configure the grid status on mount using the `initialState` prop.
9
+ You can pre-configure the grid status on mount using the `initialState` prop. Every part is optional, including each field of `columns`.
10
10
 
11
11
  ```tsx
12
12
  <DataGrid
13
+ rows={rows}
14
+ columns={columns}
13
15
  initialState={{
14
16
  sorting: {
15
17
  sortModel: [{ field: 'name', sort: 'asc' }]
16
- }
18
+ },
19
+ filter: {
20
+ filterModel: { items: [{ field: 'age', operator: '>', value: 30 }] }
21
+ },
22
+ columns: {
23
+ columnVisibilityModel: { age: false }
24
+ },
25
+ density: { density: 'compact' }
17
26
  }}
18
27
  />
19
28
  ```
20
29
 
30
+ `initialState` is read once, when the grid mounts. It seeds the grid's own (uncontrolled) state, so it has no effect on a model you also pass as a prop (`sortModel`, `filterModel`, `paginationModel`, โ€ฆ), and the `density` prop wins over `initialState.density`.
31
+
21
32
  ---
22
33
 
23
34
  ## โš“ The `useGridStateStorage` Hook
@@ -28,26 +39,81 @@ OpenGridX provides a built-in hook to simplify `localStorage` persistence.
28
39
  import { DataGrid, useGridStateStorage } from '@opencorestack/opengridx';
29
40
 
30
41
  export default function MyGrid() {
31
- const { initialState, onStateChange } = useGridStateStorage('my-app-storage-key');
42
+ const { initialState, onStateChange, clearState } = useGridStateStorage('my-app-storage-key');
32
43
 
33
44
  return (
34
- <DataGrid
35
- initialState={initialState}
36
- onStateChange={onStateChange}
37
- // ...
38
- />
45
+ <>
46
+ <button type="button" onClick={clearState}>Forget saved layout</button>
47
+ <DataGrid
48
+ rows={rows}
49
+ columns={columns}
50
+ initialState={initialState}
51
+ onStateChange={onStateChange}
52
+ />
53
+ </>
39
54
  );
40
55
  }
41
56
  ```
42
57
 
58
+ Writes are debounced (`debounceMs`, default 300 ms), and a pending write is flushed when the component unmounts. When the browser blocks storage (cookie blocking, sandboxed iframes), the hook falls back to no persistence instead of throwing.
59
+
60
+ Options (pass an object instead of the key string):
61
+
62
+ | Option | Type | Default | Description |
63
+ | :--- | :--- | :--- | :--- |
64
+ | `key` | `string` | โ€” | Storage key. |
65
+ | `debounceMs` | `number` | `300` | Delay before a state change is written. |
66
+ | `include` | `(keyof GridState)[]` | all | Only persist these parts, e.g. `['sorting', 'columns']`. |
67
+ | `storage` | `{ getItem, setItem, removeItem }` | `window.localStorage` | Any Storage-like object (e.g. `sessionStorage`). |
68
+
69
+ `clearState()` removes the saved state and cancels any pending write, so the old state is not written back afterwards. It does not reset the mounted grid, and the grid's next state change is saved again; remount the grid (for example with a new `key`) to start from defaults.
70
+
71
+ ### Server-side rendering (Next.js, Remix)
72
+
73
+ `useGridStateStorage` is client-only. It reads storage in its initial render, so on the server `initialState` is empty while the browser's first render already has the saved state: when saved state exists, React reports a hydration mismatch (the sort arrows, column order or page differ). Render the persisted grid only after mount, for example:
74
+
75
+ ```tsx
76
+ function PersistedGrid() {
77
+ const { initialState, onStateChange } = useGridStateStorage('my-app-storage-key');
78
+ return <DataGrid rows={rows} columns={columns} initialState={initialState} onStateChange={onStateChange} />;
79
+ }
80
+
81
+ export default function Page() {
82
+ const [mounted, setMounted] = useState(false);
83
+ useEffect(() => setMounted(true), []);
84
+ return mounted ? <PersistedGrid /> : <GridPlaceholder />;
85
+ }
86
+ ```
87
+
88
+ In Next.js, `dynamic(() => import('./PersistedGrid'), { ssr: false })` does the same.
89
+
90
+ ### Changing the key
91
+
92
+ The grid reads `initialState` only when it mounts. If the key can change while the grid stays on screen (a per-user or per-view key), remount the grid with the key so it starts from the new key's saved state:
93
+
94
+ ```tsx
95
+ const storageKey = `grid-${userId}`;
96
+ const { initialState, onStateChange } = useGridStateStorage(storageKey);
97
+
98
+ <DataGrid key={storageKey} rows={rows} columns={columns} initialState={initialState} onStateChange={onStateChange} />
99
+ ```
100
+
101
+ Without the remount the grid keeps its current state, and its next change is saved under the new key.
102
+
103
+ ### Server-side rendering
104
+
105
+ The hook reads storage during the first render. On the server there is no storage, so the server HTML uses the default state while the client's first render uses the saved one, and React reports a hydration mismatch. If users can have saved state, render the persisted grid on the client only (for example after mount, or with Next.js `dynamic(..., { ssr: false })`). The [migration guide, ยง26](../migration/v2-to-v3.md#26-state-persistence) has a complete example.
106
+
43
107
  ---
44
108
 
45
109
  ## ๐Ÿ“ก Tracking Changes
46
110
 
47
- Use the `onStateChange` callback to listen for any modifications to the grid state and save them to `localStorage` or a database.
111
+ Use the `onStateChange` callback to listen for modifications to the grid state and save them to `localStorage` or a database. It fires once on mount and then whenever the state's *value* changes โ€” re-rendering the grid with equal (even inline) props does not fire it, so storing the state in parent React state is safe.
48
112
 
49
113
  ```tsx
50
114
  <DataGrid
115
+ rows={rows}
116
+ columns={columns}
51
117
  onStateChange={(state) => {
52
118
  localStorage.setItem('grid-state', JSON.stringify(state));
53
119
  }}
@@ -58,9 +124,11 @@ Use the `onStateChange` callback to listen for any modifications to the grid sta
58
124
 
59
125
  ## ๐Ÿงฉ Supported State Objects
60
126
 
61
- The following features support state persistence:
127
+ The following features support state persistence (they appear in the `onStateChange` payload and are restored from `initialState`):
62
128
  - **Sorting**: `sortModel`
63
129
  - **Filtering**: `filterModel`
64
130
  - **Pagination**: `paginationModel`
65
131
  - **Columns**: `columnVisibilityModel`, `columnOrder`, `pinnedColumns`, `columnWidths`
66
132
  - **Density**: `density`
133
+
134
+ Only your own columns appear in the column state: the grid's synthetic grouping column (added by `groupingColDef`) is never included.