diff --git a/.changeset/reset-expanded-noop.md b/.changeset/reset-expanded-noop.md new file mode 100644 index 0000000000..8b704abc0f --- /dev/null +++ b/.changeset/reset-expanded-noop.md @@ -0,0 +1,5 @@ +--- +'@tanstack/table-core': patch +--- + +Make `table.resetExpanded()` a no-op when the target state already matches the current expanded state, so it no longer fires `onExpandedChange` with a new-but-equal map. `row.toggleExpanded()` and `table.toggleAllRowsExpanded()` already early-return this way. Because the core row model auto-resets `expanded` on every `data` reference change, the unguarded write could drive a controlled table with an unstable `data` reference into an unbounded render loop. diff --git a/docs/reference/static-functions/functions/getDefaultExpandedState.md b/docs/reference/static-functions/functions/getDefaultExpandedState.md index 4cc98299d7..c42b8212dc 100644 --- a/docs/reference/static-functions/functions/getDefaultExpandedState.md +++ b/docs/reference/static-functions/functions/getDefaultExpandedState.md @@ -9,7 +9,7 @@ title: getDefaultExpandedState function getDefaultExpandedState(): ExpandedState; ``` -Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:22](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L22) +Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:23](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L23) Creates the default expanded state. diff --git a/docs/reference/static-functions/functions/row_getCanExpand.md b/docs/reference/static-functions/functions/row_getCanExpand.md index eb5e84e379..d4280eba1a 100644 --- a/docs/reference/static-functions/functions/row_getCanExpand.md +++ b/docs/reference/static-functions/functions/row_getCanExpand.md @@ -9,7 +9,7 @@ title: row_getCanExpand function row_getCanExpand(row): boolean; ``` -Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:378](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L378) +Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:385](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L385) Checks whether this row can be expanded. diff --git a/docs/reference/static-functions/functions/row_getIsAllParentsExpanded.md b/docs/reference/static-functions/functions/row_getIsAllParentsExpanded.md index e9d9add8f8..2a417cf51a 100644 --- a/docs/reference/static-functions/functions/row_getIsAllParentsExpanded.md +++ b/docs/reference/static-functions/functions/row_getIsAllParentsExpanded.md @@ -9,7 +9,7 @@ title: row_getIsAllParentsExpanded function row_getIsAllParentsExpanded(row): boolean; ``` -Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:398](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L398) +Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:405](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L405) Checks whether every ancestor of this row is expanded. diff --git a/docs/reference/static-functions/functions/row_getIsExpanded.md b/docs/reference/static-functions/functions/row_getIsExpanded.md index b54f1e7ebc..f569d38984 100644 --- a/docs/reference/static-functions/functions/row_getIsExpanded.md +++ b/docs/reference/static-functions/functions/row_getIsExpanded.md @@ -9,7 +9,7 @@ title: row_getIsExpanded function row_getIsExpanded(row): boolean; ``` -Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:343](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L343) +Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:350](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L350) Checks whether this row is expanded. diff --git a/docs/reference/static-functions/functions/row_getToggleExpandedHandler.md b/docs/reference/static-functions/functions/row_getToggleExpandedHandler.md index 22537d3c1f..4c79d67686 100644 --- a/docs/reference/static-functions/functions/row_getToggleExpandedHandler.md +++ b/docs/reference/static-functions/functions/row_getToggleExpandedHandler.md @@ -9,7 +9,7 @@ title: row_getToggleExpandedHandler function row_getToggleExpandedHandler(row): () => void; ``` -Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:423](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L423) +Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:430](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L430) Creates a row control handler that toggles this row's expanded state. diff --git a/docs/reference/static-functions/functions/row_toggleExpanded.md b/docs/reference/static-functions/functions/row_toggleExpanded.md index 2a262152d8..b6ca311a19 100644 --- a/docs/reference/static-functions/functions/row_toggleExpanded.md +++ b/docs/reference/static-functions/functions/row_toggleExpanded.md @@ -9,7 +9,7 @@ title: row_toggleExpanded function row_toggleExpanded(row, expanded?): void; ``` -Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:284](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L284) +Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:291](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L291) Expands or collapses this row. diff --git a/docs/reference/static-functions/functions/table_autoResetExpanded.md b/docs/reference/static-functions/functions/table_autoResetExpanded.md index ae4cbf4d0e..71c4a02228 100644 --- a/docs/reference/static-functions/functions/table_autoResetExpanded.md +++ b/docs/reference/static-functions/functions/table_autoResetExpanded.md @@ -9,7 +9,7 @@ title: table_autoResetExpanded function table_autoResetExpanded(table): void; ``` -Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:38](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L38) +Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:39](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L39) Schedules an expanded-state reset after row-structure changes. diff --git a/docs/reference/static-functions/functions/table_getCanSomeRowsExpand.md b/docs/reference/static-functions/functions/table_getCanSomeRowsExpand.md index 8d8555ce3d..a37f89dc75 100644 --- a/docs/reference/static-functions/functions/table_getCanSomeRowsExpand.md +++ b/docs/reference/static-functions/functions/table_getCanSomeRowsExpand.md @@ -9,7 +9,7 @@ title: table_getCanSomeRowsExpand function table_getCanSomeRowsExpand(table): boolean; ``` -Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:145](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L145) +Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:152](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L152) Checks whether at least one pre-paginated row can expand. diff --git a/docs/reference/static-functions/functions/table_getExpandedDepth.md b/docs/reference/static-functions/functions/table_getExpandedDepth.md index b65b28ec64..8bf98715b6 100644 --- a/docs/reference/static-functions/functions/table_getExpandedDepth.md +++ b/docs/reference/static-functions/functions/table_getExpandedDepth.md @@ -9,7 +9,7 @@ title: table_getExpandedDepth function table_getExpandedDepth(table): number; ``` -Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:245](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L245) +Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:252](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L252) Computes the deepest expanded row id depth. diff --git a/docs/reference/static-functions/functions/table_getIsAllRowsExpanded.md b/docs/reference/static-functions/functions/table_getIsAllRowsExpanded.md index a0b395cf8d..569ddd8c74 100644 --- a/docs/reference/static-functions/functions/table_getIsAllRowsExpanded.md +++ b/docs/reference/static-functions/functions/table_getIsAllRowsExpanded.md @@ -9,7 +9,7 @@ title: table_getIsAllRowsExpanded function table_getIsAllRowsExpanded(table): boolean; ``` -Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:202](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L202) +Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:209](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L209) Checks whether every expandable row in the current row model is expanded. diff --git a/docs/reference/static-functions/functions/table_getIsSomeRowsExpanded.md b/docs/reference/static-functions/functions/table_getIsSomeRowsExpanded.md index a65d9d6348..9b1c418947 100644 --- a/docs/reference/static-functions/functions/table_getIsSomeRowsExpanded.md +++ b/docs/reference/static-functions/functions/table_getIsSomeRowsExpanded.md @@ -9,7 +9,7 @@ title: table_getIsSomeRowsExpanded function table_getIsSomeRowsExpanded(table): boolean; ``` -Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:181](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L181) +Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:188](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L188) Checks whether any row is expanded. diff --git a/docs/reference/static-functions/functions/table_getToggleAllRowsExpandedHandler.md b/docs/reference/static-functions/functions/table_getToggleAllRowsExpandedHandler.md index 0d25c3b2b2..b2b2c04394 100644 --- a/docs/reference/static-functions/functions/table_getToggleAllRowsExpandedHandler.md +++ b/docs/reference/static-functions/functions/table_getToggleAllRowsExpandedHandler.md @@ -9,7 +9,7 @@ title: table_getToggleAllRowsExpandedHandler function table_getToggleAllRowsExpandedHandler(table): (_e) => void; ``` -Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:162](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L162) +Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:169](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L169) Creates an event handler that toggles all rows expanded. diff --git a/docs/reference/static-functions/functions/table_resetExpanded.md b/docs/reference/static-functions/functions/table_resetExpanded.md index f90d997ebc..dda2f8ba80 100644 --- a/docs/reference/static-functions/functions/table_resetExpanded.md +++ b/docs/reference/static-functions/functions/table_resetExpanded.md @@ -9,13 +9,17 @@ title: table_resetExpanded function table_resetExpanded(table, defaultState?): void; ``` -Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:116](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L116) +Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:121](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L121) Resets `expanded` to the configured initial state or feature default. With no argument, the reset clones `table.initialState.expanded` when it exists. Passing `true` ignores initial state and resets to `{}`. +The call is a no-op (no `onExpandedChange`) when the target state already +matches the current state, so an auto-reset on a table with nothing expanded +does not publish a new-but-equal map. + ## Type Parameters ### TFeatures diff --git a/docs/reference/static-functions/functions/table_setExpanded.md b/docs/reference/static-functions/functions/table_setExpanded.md index 78c4dca8c5..9c34d7643c 100644 --- a/docs/reference/static-functions/functions/table_setExpanded.md +++ b/docs/reference/static-functions/functions/table_setExpanded.md @@ -9,7 +9,7 @@ title: table_setExpanded function table_setExpanded(table, updater): void; ``` -Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:66](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L66) +Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:67](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L67) Routes an expanded-state updater through the table's expanded change handler. diff --git a/docs/reference/static-functions/functions/table_toggleAllRowsExpanded.md b/docs/reference/static-functions/functions/table_toggleAllRowsExpanded.md index daa689cc70..1f7b0d71b6 100644 --- a/docs/reference/static-functions/functions/table_toggleAllRowsExpanded.md +++ b/docs/reference/static-functions/functions/table_toggleAllRowsExpanded.md @@ -9,7 +9,7 @@ title: table_toggleAllRowsExpanded function table_toggleAllRowsExpanded(table, expanded?): void; ``` -Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:88](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L88) +Defined in: [features/row-expanding/rowExpandingFeature.utils.ts:89](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L89) Expands or collapses every row. diff --git a/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts b/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts index 832d98138f..bab350d5a7 100644 --- a/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts +++ b/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts @@ -1,3 +1,4 @@ +import { shallow } from '@tanstack/store' import { cloneState, hasOwn, makeObjectMap } from '../../utils' import type { RowData, Updater } from '../../types/type-utils' import type { TableFeatures } from '../../types/TableFeatures' @@ -107,6 +108,10 @@ export function table_toggleAllRowsExpanded< * With no argument, the reset clones `table.initialState.expanded` when it * exists. Passing `true` ignores initial state and resets to `{}`. * + * The call is a no-op (no `onExpandedChange`) when the target state already + * matches the current state, so an auto-reset on a table with nothing expanded + * does not publish a new-but-equal map. + * * @example * ```ts * table_resetExpanded(table) @@ -117,18 +122,20 @@ export function table_resetExpanded< TFeatures extends TableFeatures, TData extends RowData, >(table: Table_Internal, defaultState?: boolean) { + const currentExpanded = table.atoms.expanded?.get() ?? {} const initialExpanded = table.initialState.expanded - table_setExpanded( - table, - defaultState - ? makeObjectMap() - : initialExpanded === true - ? true - : Object.assign( - makeObjectMap(), - cloneState(initialExpanded ?? {}), - ), - ) + const newExpanded: ExpandedState = defaultState + ? makeObjectMap() + : initialExpanded === true + ? true + : Object.assign( + makeObjectMap(), + cloneState(initialExpanded ?? {}), + ) + + if (shallow(currentExpanded, newExpanded)) return + + table_setExpanded(table, newExpanded) } /** diff --git a/packages/table-core/tests/unit/features/row-expanding/rowExpandingFeature.utils.test.ts b/packages/table-core/tests/unit/features/row-expanding/rowExpandingFeature.utils.test.ts index edf1aeeb5b..067393c44b 100644 --- a/packages/table-core/tests/unit/features/row-expanding/rowExpandingFeature.utils.test.ts +++ b/packages/table-core/tests/unit/features/row-expanding/rowExpandingFeature.utils.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest' import { constructTable, createExpandedRowModel, + functionalUpdate, rowExpandingFeature, } from '../../../../src' import { @@ -25,7 +26,12 @@ import { testFeatures } from '../../../fixtures/features' import { generateTestColumnDefs } from '../../../fixtures/data/generateTestColumnDefs' import { generateTestData } from '../../../fixtures/data/generateTestData' import { getUpdaterResult } from '../../../helpers/testUtils' -import type { ExpandedState, Table, TableOptions } from '../../../../src' +import type { + ExpandedState, + Table, + TableOptions, + Updater, +} from '../../../../src' import type { Person } from '../../../fixtures/data/types' const features = testFeatures({ @@ -88,6 +94,7 @@ describe('table_resetExpanded', () => { onExpandedChange, initialState: { expanded: { '0': true } }, }) + table.baseAtoms.expanded.set({ '0': true, '1': true }) table_resetExpanded(table) @@ -100,11 +107,67 @@ describe('table_resetExpanded', () => { onExpandedChange, initialState: { expanded: true }, }) + table.baseAtoms.expanded.set({ '0': true }) table_resetExpanded(table) expect(onExpandedChange).toHaveBeenCalledWith(true) }) + + it('should reset when the expanded ids differ at the same count', () => { + const onExpandedChange = vi.fn() + const table = makeTable({ + onExpandedChange, + initialState: { expanded: { '0': true } }, + }) + table.baseAtoms.expanded.set({ '1': true }) + + table_resetExpanded(table) + + expect(onExpandedChange).toHaveBeenCalledWith({ '0': true }) + }) + + it('should be a no-op when nothing is expanded', () => { + const onExpandedChange = vi.fn() + const table = makeTable({ onExpandedChange }) + + table_resetExpanded(table) + + expect(onExpandedChange).not.toHaveBeenCalled() + }) + + it('should be a no-op when the expanded map is already at the target', () => { + const onExpandedChange = vi.fn() + const table = makeTable({ + onExpandedChange, + initialState: { expanded: { '0': true } }, + }) + + table_resetExpanded(table) + + expect(onExpandedChange).not.toHaveBeenCalled() + }) + + it('should be a no-op when already in the expanded-all initial state', () => { + const onExpandedChange = vi.fn() + const table = makeTable({ + onExpandedChange, + initialState: { expanded: true }, + }) + + table_resetExpanded(table) + + expect(onExpandedChange).not.toHaveBeenCalled() + }) + + it('should be a no-op when defaultState is true and nothing is expanded', () => { + const onExpandedChange = vi.fn() + const table = makeTable({ onExpandedChange }) + + table_resetExpanded(table, true) + + expect(onExpandedChange).not.toHaveBeenCalled() + }) }) describe('table_toggleAllRowsExpanded', () => { @@ -538,4 +601,32 @@ describe('table_autoResetExpanded', () => { expect(onExpandedChange).not.toHaveBeenCalled() }) + + it('should not loop when a data identity change resets controlled state that already matches', async () => { + // Stands in for a framework render loop: the auto-reset publishes expanded + // state, the consumer re-renders with a fresh `data` reference, and the core + // row model recomputes and auto-resets again + let expanded: ExpandedState = {} + let table: Table + const onExpandedChange = vi.fn((updater: Updater) => { + expanded = functionalUpdate(updater, expanded) + // stop feeding the loop so a regression fails the assertion below + // instead of hanging the suite + if (onExpandedChange.mock.calls.length > 5) return + table.setOptions((prev) => ({ + ...prev, + data: [...prev.data], + state: { ...prev.state, expanded }, + })) + table.getCoreRowModel() + }) + table = makeTable({ state: { expanded }, onExpandedChange }) + table.getCoreRowModel() + + table.setOptions((prev) => ({ ...prev, data: [...prev.data] })) + table.getCoreRowModel() + await flushMicrotasks() + + expect(onExpandedChange).not.toHaveBeenCalled() + }) })