Skip to content

Commit 23fae26

Browse files
authored
fix: state undefined (#6497)
1 parent 6fe1f85 commit 23fae26

3 files changed

Lines changed: 83 additions & 4 deletions

File tree

packages/table-core/src/core/table/constructTable.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,15 @@ export function constructTable<
159159
const controlledState = options.state as
160160
Record<string, unknown> | undefined
161161

162-
return controlledState && hasOwn(controlledState, key)
163-
? controlledState[key]
164-
: reactiveState
162+
if (controlledState && hasOwn(controlledState, key)) {
163+
// An explicitly `undefined` controlled slice falls back to the
164+
// slice's initial state so required snapshot slices stay defined.
165+
const controlledValue = controlledState[key]
166+
return controlledValue === undefined
167+
? table.initialState[key]
168+
: controlledValue
169+
}
170+
return reactiveState
165171
},
166172
{ debugName: `table/atoms/${key}` },
167173
)

packages/table-core/src/core/table/coreTablesFeature.utils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,13 @@ export function table_syncExternalStateToBaseAtoms<
4747
continue
4848
}
4949

50-
const externalState = state[key as keyof typeof state]
50+
// An explicitly `undefined` controlled slice syncs the slice's
51+
// initial state instead so the base atom never holds `undefined`.
52+
const rawExternalState = state[key as keyof typeof state]
53+
const externalState =
54+
rawExternalState === undefined
55+
? (table.initialState as Record<string, unknown>)[key]
56+
: rawExternalState
5157
const currentState = table._reactivity.untrack(() => baseAtom.get())
5258
if (!compare(currentState, externalState)) {
5359
baseAtom.set(() => externalState)

packages/table-core/tests/unit/core/tableAtoms.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { describe, expect, it, vi } from 'vitest'
22
import { batch, createAtom } from '@tanstack/store'
33
import {
4+
columnFilteringFeature,
45
constructTable,
6+
globalFilteringFeature,
57
rowPaginationFeature,
68
rowSelectionFeature,
79
rowSortingFeature,
@@ -180,6 +182,71 @@ describe('three-layer atom architecture', () => {
180182
})
181183
})
182184

185+
describe('explicitly undefined controlled state (#5909)', () => {
186+
it('falls back to the slice initial default at construct', () => {
187+
const table = makeTable({ state: { sorting: undefined } })
188+
expect(table.atoms.sorting.get()).toEqual([])
189+
expect(table.store.state.sorting).toEqual([])
190+
expect(table.baseAtoms.sorting.get()).toEqual([])
191+
})
192+
193+
it('falls back to user-provided initialState', () => {
194+
const table = makeTable({
195+
initialState: { sorting: [{ id: 'name', desc: true }] },
196+
state: { sorting: undefined },
197+
})
198+
expect(table.store.state.sorting).toEqual([{ id: 'name', desc: true }])
199+
})
200+
201+
it('does not poison baseAtoms when a controlled slice becomes undefined', () => {
202+
const controlled: SortingState = [{ id: 'name', desc: false }]
203+
const table = makeTable({ state: { sorting: controlled } })
204+
const internalTable = table as unknown as Table_Internal<
205+
typeof features,
206+
any
207+
>
208+
expect(table.baseAtoms.sorting.get()).toBe(controlled)
209+
210+
table_setOptions(internalTable, (options) => ({
211+
...options,
212+
state: { sorting: undefined },
213+
}))
214+
expect(table.store.state.sorting).toEqual([])
215+
expect(table.baseAtoms.sorting.get()).toEqual([])
216+
217+
// removing the key returns the slice to uncontrolled internal writes
218+
table_setOptions(internalTable, (options) => ({
219+
...options,
220+
state: {},
221+
}))
222+
table.setSorting([{ id: 'age', desc: true }])
223+
expect(table.store.state.sorting).toEqual([{ id: 'age', desc: true }])
224+
})
225+
226+
it('controlled globalFilter can still be cleared with undefined', () => {
227+
const gfFeatures = testFeatures({
228+
columnFilteringFeature,
229+
globalFilteringFeature,
230+
})
231+
const table = constructTable({
232+
features: gfFeatures,
233+
columns: [],
234+
data: [],
235+
state: { globalFilter: 'search' },
236+
})
237+
expect(table.store.state.globalFilter).toBe('search')
238+
239+
table_setOptions(
240+
table as unknown as Table_Internal<typeof gfFeatures, any>,
241+
(options) => ({
242+
...options,
243+
state: { globalFilter: undefined },
244+
}),
245+
)
246+
expect(table.store.state.globalFilter).toBeUndefined()
247+
})
248+
})
249+
183250
describe('store (readonly flat derived)', () => {
184251
it('has identical public shape to TableState', () => {
185252
const table = makeTable()

0 commit comments

Comments
 (0)