Skip to content

fix(table-core): don't fire onExpandedChange when resetExpanded is a no-op - #6519

Open
Faithfinder wants to merge 1 commit into
TanStack:mainfrom
Faithfinder:fix-reset-expanded-noop
Open

fix(table-core): don't fire onExpandedChange when resetExpanded is a no-op#6519
Faithfinder wants to merge 1 commit into
TanStack:mainfrom
Faithfinder:fix-reset-expanded-noop

Conversation

@Faithfinder

@Faithfinder Faithfinder commented Aug 5, 2026

Copy link
Copy Markdown

table_resetExpanded builds a fresh state object and always calls onExpandedChange, without comparing against the current state. Every sibling compares first:

Since #6499 wired the expansion auto-reset into createCoreRowModel, that unguarded write fires on every data reference change:

data identity changes → core row model recomputes → table_autoResetExpandedtable_resetExpandedonExpandedChange(newEmptyMap) → consumer re-renders → new data reference → repeat.

Measured with this repo's react-table on React 19.2, controlled expanded via useState, getRowCanExpand: () => true, and one row-model read during render, capped at 50 renders:

data renders onExpandedChange calls
items ?? [] 50 (capped) 48
items ?? [] + this fix 2 0
items ?? STABLE_EMPTY_ARRAY 2 0
items ?? [] + autoResetExpanded: false 2 0

Stable data is 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 passed table.initialState?.expanded through by reference, so setExpanded(sameRef) hit the identity check in useState. v9 clones it, so the reference is always new.

Change

table_resetExpanded compares its target against table.atoms.expanded?.get() (as row_toggleExpanded already does) and returns early when they match — true by identity, maps key-by-key, both defaultState branches.

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

    • Prevented redundant expanded-state updates when resetting a table whose expanded state is already synchronized.
    • Avoided unnecessary change callbacks and potential render loops when data references change.
    • Preserved correct reset behavior when expanded rows differ.
  • Documentation

    • Updated expansion API source links and documented the no-op reset behavior.

…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.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 481be2ba-05ea-46db-989f-195d65d7d27a

📥 Commits

Reviewing files that changed from the base of the PR and between 50feff8 and 59e8367.

📒 Files selected for processing (14)
  • .changeset/reset-expanded-noop.md
  • docs/reference/static-functions/functions/row_getCanExpand.md
  • docs/reference/static-functions/functions/row_getIsAllParentsExpanded.md
  • docs/reference/static-functions/functions/row_getIsExpanded.md
  • docs/reference/static-functions/functions/row_getToggleExpandedHandler.md
  • docs/reference/static-functions/functions/row_toggleExpanded.md
  • docs/reference/static-functions/functions/table_getCanSomeRowsExpand.md
  • docs/reference/static-functions/functions/table_getExpandedDepth.md
  • docs/reference/static-functions/functions/table_getIsAllRowsExpanded.md
  • docs/reference/static-functions/functions/table_getIsSomeRowsExpanded.md
  • docs/reference/static-functions/functions/table_getToggleAllRowsExpandedHandler.md
  • docs/reference/static-functions/functions/table_resetExpanded.md
  • packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts
  • packages/table-core/tests/unit/features/row-expanding/rowExpandingFeature.utils.test.ts

📝 Walkthrough

Walkthrough

table_resetExpanded now skips onExpandedChange when the computed reset state equals the current expanded state. Tests cover map and expanded-all states, controlled auto-reset behavior, and updated documentation links.

Changes

Expanded reset behavior

Layer / File(s) Summary
Reset equality guard and references
packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts, docs/reference/static-functions/functions/*, .changeset/reset-expanded-noop.md
table_resetExpanded compares expanded-all values and expanded row maps before publishing changes. Documentation links and reset behavior details were updated.
Reset and controlled-state regression coverage
packages/table-core/tests/unit/features/row-expanding/rowExpandingFeature.utils.test.ts
Tests cover differing and matching expanded states, expanded-all resets, default resets, and controlled auto-reset after data identity changes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: kevinvandy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the problem, motivation, implementation, regression scenario, and test updates, but it omits the template headings and checklist.
Title check ✅ Passed The title is concise and accurately identifies the primary change: avoiding onExpandedChange when resetExpanded is a no-op.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant