Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/reset-expanded-noop.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ title: row_getCanExpand
function row_getCanExpand<TFeatures, TData>(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:399](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L399)

Checks whether this row can be expanded.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ title: row_getIsAllParentsExpanded
function row_getIsAllParentsExpanded<TFeatures, TData>(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:419](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L419)

Checks whether every ancestor of this row is expanded.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ title: row_getIsExpanded
function row_getIsExpanded<TFeatures, TData>(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:364](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L364)

Checks whether this row is expanded.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ title: row_getToggleExpandedHandler
function row_getToggleExpandedHandler<TFeatures, TData>(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:444](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L444)

Creates a row control handler that toggles this row's expanded state.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ title: row_toggleExpanded
function row_toggleExpanded<TFeatures, TData>(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:305](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L305)

Expands or collapses this row.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ title: table_getCanSomeRowsExpand
function table_getCanSomeRowsExpand<TFeatures, TData>(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:166](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L166)

Checks whether at least one pre-paginated row can expand.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ title: table_getExpandedDepth
function table_getExpandedDepth<TFeatures, TData>(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:266](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L266)

Computes the deepest expanded row id depth.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ title: table_getIsAllRowsExpanded
function table_getIsAllRowsExpanded<TFeatures, TData>(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:223](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L223)

Checks whether every expandable row in the current row model is expanded.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ title: table_getIsSomeRowsExpanded
function table_getIsSomeRowsExpanded<TFeatures, TData>(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:202](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L202)

Checks whether any row is expanded.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ title: table_getToggleAllRowsExpandedHandler
function table_getToggleAllRowsExpandedHandler<TFeatures, TData>(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:183](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L183)

Creates an event handler that toggles all rows expanded.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ title: table_resetExpanded
function table_resetExpanded<TFeatures, TData>(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:120](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts#L120)

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,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)
Expand All @@ -117,18 +121,35 @@ export function table_resetExpanded<
TFeatures extends TableFeatures,
TData extends RowData,
>(table: Table_Internal<TFeatures, TData>, defaultState?: boolean) {
const currentExpanded = table.atoms.expanded?.get() ?? {}
const initialExpanded = table.initialState.expanded
table_setExpanded(
table,
defaultState
? makeObjectMap()
: initialExpanded === true
? true
: Object.assign(
makeObjectMap<boolean | undefined>(),
cloneState(initialExpanded ?? {}),
),
)
const newExpanded: ExpandedState = defaultState
? makeObjectMap()
: initialExpanded === true
? true
: Object.assign(
makeObjectMap<boolean | undefined>(),
cloneState(initialExpanded ?? {}),
)

if (isSameExpandedState(currentExpanded, newExpanded)) return

table_setExpanded(table, newExpanded)
}

function isSameExpandedState(a: ExpandedState, b: ExpandedState): boolean {
if (a === true || b === true) return a === b

const aKeys = Object.keys(a)

if (aKeys.length !== Object.keys(b).length) return false

for (let i = 0; i < aKeys.length; i++) {
const key = aKeys[i]!
if (!hasOwn(b, key) || a[key] !== b[key]) return false
}

return true
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
import {
constructTable,
createExpandedRowModel,
functionalUpdate,
rowExpandingFeature,
} from '../../../../src'
import {
Expand All @@ -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({
Expand Down Expand Up @@ -88,6 +94,7 @@ describe('table_resetExpanded', () => {
onExpandedChange,
initialState: { expanded: { '0': true } },
})
table.baseAtoms.expanded.set({ '0': true, '1': true })

table_resetExpanded(table)

Expand All @@ -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', () => {
Expand Down Expand Up @@ -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<typeof features, Person>
const onExpandedChange = vi.fn((updater: Updater<ExpandedState>) => {
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()
})
})