fix(table-core): don't fire onExpandedChange when resetExpanded is a no-op - #6519
fix(table-core): don't fire onExpandedChange when resetExpanded is a no-op#6519Faithfinder wants to merge 2 commits into
Conversation
…no-op table_resetExpanded built a fresh state object and always routed it through onExpandedChange, without comparing against the current expanded state. Its siblings all compare first: table_resetPageIndex and table_resetPageSize early-return when the value already matches, and row_toggleExpanded and table_toggleAllRowsExpanded gained the same guard in TanStack#6501. Since TanStack#6499 wired the expansion auto-reset into createCoreRowModel, that unguarded write fires on every data reference change. For a controlled table whose data is not referentially stable, the new-but-equal map re-renders the consumer, which produces another new data reference, and the cycle repeats without bound. Compare the target state against table.atoms.expanded?.get() and return early when they match, for both defaultState branches. Expanded-all compares by identity, maps key-by-key.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (16)
🚧 Files skipped from review as they are similar to previous changes (12)
📝 WalkthroughWalkthrough
ChangesExpanded reset behavior
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
we already have code for shallow checking in tanstack store. I might look into more broadly applying that in our setState functions instead of re-implementing shallow checks throughout the entire codebase like this. |
Replace the hand-rolled expanded-state comparison with shallow from @tanstack/store, which table-core already uses as the table store's compare in constructTable. Object.is covers the expanded-all identity case, the typeof guard covers true against a map, and Object.keys plus hasOwnProperty keep null-prototype state maps safe.
Swapped to existing shallow compare function. As for moving it to
|
table_resetExpandedbuilds a fresh state object and always callsonExpandedChange, without comparing against the current state. Every sibling compares first:table_resetPageIndex/table_resetPageSize— early-return when the value already matchesrow_toggleExpanded/table_toggleAllRowsExpanded— early-return, added in fix(table-core): correct expanded/paginated state contents and sorting toggle defaults #6501table_resetExpanded— no guardSince #6499 wired the expansion auto-reset into
createCoreRowModel, that unguarded write fires on everydatareference change:dataidentity changes → core row model recomputes →table_autoResetExpanded→table_resetExpanded→onExpandedChange(newEmptyMap)→ consumer re-renders → newdatareference → repeat.Measured with this repo's
react-tableon React 19.2, controlledexpandedviauseState,getRowCanExpand: () => true, and one row-model read during render, capped at 50 renders:dataonExpandedChangecallsitems ?? []items ?? []+ this fixitems ?? STABLE_EMPTY_ARRAYitems ?? []+autoResetExpanded: falseStable
datais documented, and the loop needs an unstable reference — this is a user mistake in the first instance. The case for guarding anyway is that the punishment is disproportionate and near-undiagnosable: nothing in the stack points at expansion, and the tab is too unresponsive to profile. The pagination resets already absorb exactly this mistake, and v8 absorbed it too — it passedtable.initialState?.expandedthrough by reference, sosetExpanded(sameRef)hit the identity check inuseState. v9 clones it, so the reference is always new.Change
table_resetExpandedcompares its target againsttable.atoms.expanded?.get()(asrow_toggleExpandedalready does) and returns early when they match —trueby identity, maps key-by-key, bothdefaultStatebranches.Two existing tests asserted that the reset fires when the current state already equals the target. They now diverge the state first, so they still cover the reset value.
Summary by CodeRabbit
Bug Fixes
Documentation
Tests