Repository navigation
In TreeDataGrid, find the parent group row with a binary search and replace getOrInsertComputed - #4227
Merged
Conversation
`TreeDataGrid` no longer asks for a function that groups a set of rows into a `Record<string, readonly R[]>`. Instead, `getRowGroupKey(row, columnKey)` returns the group key of a single row, and the grid builds the group tree itself in a single pass over the rows. Grouping and flattening are now done in one recursion which also returns the fully-expanded row count, so `startRowIndex` and `aria-rowcount` keep their previous meaning. Note: groups are now ordered by the first row that belongs to them, whereas `Object.entries()` used to sort integer-like group keys numerically. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RQUA3cWdaFLZSQwKAGCC3x
It defaults to `String(row[columnKey])`, which is what most grids need, so grouping now only requires `groupBy`, `expandedGroupIds` and `onExpandedGroupIdsChange`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RQUA3cWdaFLZSQwKAGCC3x
Grouping the rows only depends on `rows`, `groupBy` and `getRowGroupKey`, while flattening them depends on the expanded state. Keeping both in a single `useMemo` meant that expanding or collapsing a group re-grouped every row. `startRowIndex` and the total row count do not depend on the expanded state either, so they are computed while grouping, and collapsed groups no longer have to be traversed when the rows are flattened. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RQUA3cWdaFLZSQwKAGCC3x
Group rows are only ever created by `TreeDataGrid`, so a single set can be shared by every instance instead of being rebuilt on every flatten. That makes `isGroupRow` a stable module-level function, which drops it from four dependency arrays and lets the `rows` memo return the rows alone. `rowHeight` is no longer recomputed when a group is expanded or collapsed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RQUA3cWdaFLZSQwKAGCC3x
…th-getRowGroupKey # Conflicts: # README.md # src/TreeDataGrid.tsx
Add a test that integer-like group keys keep the order of their first row, instead of being sorted numerically like `Object.entries()` used to. The row grouping demo only sorted its rows by country, so its year groups would have come out in random order. Sort the rows by year too. Add a performance note to `getRowGroupKey`, as a new function identity groups all the rows again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…th-getRowGroupKey
The groups are only mutated while the rows are being grouped, so build them with a mutable type and return them from the `useMemo` as a `ReadonlyMap` of readonly groups, like `useCalculatedColumns` does with its columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `useMemo` already depends on the rows, so it can return the row count when there is nothing to group too, instead of falling back to `rawRows.length` on every render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Function props are moving to `getX` names, which `getRowGroupKey` already follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…th-getRowGroupKey Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pushing the rows of an expanded group with `push(...childRows)` throws a RangeError once the group has more rows than browsers accept as function arguments, so push them one by one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Destructuring in the `for...of` head made React Compiler skip the component. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Groups are ordered by the first row that belongs to them, so nested groups, e.g., years under sports, were out of order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The row count doesn't depend on the expanded state, so it alone didn't show that the group was flattened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each rendered row used to look for its parent group row with `rows.indexOf`, a backward walk, and `childRows.indexOf`, so a render scanned up to all the rows for each visible row. The group row indexes are now recorded while flattening, and the parent is found with a binary search over them. The row's position in its group follows from the index of its group row, and the default row keys are computed in `renderRow`, where the row index is known. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nstepien
added this pull request to stack #4228
October 23, 2026 19:52
amanmahajan7
approved these changes
Oct 23, 2026
amanmahajan7
left a comment
Collaborator
There was a problem hiding this comment.
Looks like CI is flaky. It keeps timing out
Collaborator
Author
|
Yes installing the webkit dependencies can be slow at times, not sure if it's an issue with GH or the remote package repos of the deps. |
Base automatically changed from
replace-rowGrouper-with-getRowGroupKey
to
main
October 23, 2026 20:46
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
V8 inlines `Map.prototype.get` in optimized code but not `getOrInsertComputed`, and the callback was created for every row and level, which doubled the young generation garbage collections while grouping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TreeDataGridTreeDataGrid, find the parent group row with a binary search and replace getOrInsertComputed
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4227 +/- ##
=======================================
Coverage 98.95% 98.96%
=======================================
Files 46 46
Lines 2017 2030 +13
Branches 787 792 +5
=======================================
+ Hits 1996 2009 +13
Misses 21 21
🚀 New features to boost your workflow:
|
Each group on the last level allocated a map that always stayed empty. With small groups, e.g., about 2 rows per group, grouping 1M rows now takes about 20% less time and retains 40% less memory, and large groups are unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TreeDataGridfound each rendered row's parent group row withrows.indexOf(row), a backward walk over the rows, andchildRows.indexOf(row). Without arowKeyGetter, its ownrowKeyGetterdid the same lookup again. Scrolling near the end of a large group therefore scanned up to all the rows for every visible row.mainhas the same problem.Changes
getParentRowAndIndexnow takes a row index and runs a binary search over them.rowIdx - parentIdx - 1, andchildRows.indexOfis gone.TreeDataGridonly passes arowKeyGettertoDataGridwhen one is provided. Otherwise,renderRowcomputes the same keys as before: the group ID for group rows, andstartRowIndex + groupIndex + 1for the rows of a group.DataGridonly readsrowKeyGetterfor row keys and selection, and selection already requires arowKeyGetter.getParentRowAndIndexis a plain function, since no memo depends on it anymore.Performance
Median React Profiler duration of a scroll render, over 20 scrolls near the end of the rows, in Chromium with React's production profiling build (
react-dom/profiling) and React Compiler:rowKeyGetterBefore, nearly all of that time was spent in
TreeDataGrid's own scans, so the times were about the same with React's development build. After, the remaining time is React's, which is about 6 times slower in development.Grouping
Rows are now grouped with
getandsetinstead ofgetOrInsertComputed, which was slower for two reasons:Map.prototype.getas a hash lookup, but have no such reduction forgetOrInsertComputed, so each call goes through the builtin.Median time to group 1M rows in Node.js 26 (V8 14.6), each version in its own process:
getOrInsertComputedgetandsetThe groups on the last level also no longer create a map of subgroups that always stayed empty, which mostly helps with small groups. Median time and retained heap when grouping 1M rows on one level:
Tests
A new test checks that, without a
rowKeyGetter, a row keeps its element when an earlier group is expanded. It fails if the rows of a group are keyed by their index.vp run cipasses.🤖 Generated with Claude Code