From 81585b657fbd74140293703750d65bf34cc03a18 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Sun, 2 Aug 2026 13:27:11 -0500 Subject: [PATCH] fix: state undefined --- .../src/core/table/constructTable.ts | 12 +++- .../src/core/table/coreTablesFeature.utils.ts | 8 ++- .../tests/unit/core/tableAtoms.test.ts | 67 +++++++++++++++++++ 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/packages/table-core/src/core/table/constructTable.ts b/packages/table-core/src/core/table/constructTable.ts index b1838697b5..7a6519d354 100644 --- a/packages/table-core/src/core/table/constructTable.ts +++ b/packages/table-core/src/core/table/constructTable.ts @@ -159,9 +159,15 @@ export function constructTable< const controlledState = options.state as Record | undefined - return controlledState && hasOwn(controlledState, key) - ? controlledState[key] - : reactiveState + if (controlledState && hasOwn(controlledState, key)) { + // An explicitly `undefined` controlled slice falls back to the + // slice's initial state so required snapshot slices stay defined. + const controlledValue = controlledState[key] + return controlledValue === undefined + ? table.initialState[key] + : controlledValue + } + return reactiveState }, { debugName: `table/atoms/${key}` }, ) diff --git a/packages/table-core/src/core/table/coreTablesFeature.utils.ts b/packages/table-core/src/core/table/coreTablesFeature.utils.ts index 73f3aea4b5..ddc27b1c14 100644 --- a/packages/table-core/src/core/table/coreTablesFeature.utils.ts +++ b/packages/table-core/src/core/table/coreTablesFeature.utils.ts @@ -47,7 +47,13 @@ export function table_syncExternalStateToBaseAtoms< continue } - const externalState = state[key as keyof typeof state] + // An explicitly `undefined` controlled slice syncs the slice's + // initial state instead so the base atom never holds `undefined`. + const rawExternalState = state[key as keyof typeof state] + const externalState = + rawExternalState === undefined + ? (table.initialState as Record)[key] + : rawExternalState const currentState = table._reactivity.untrack(() => baseAtom.get()) if (!compare(currentState, externalState)) { baseAtom.set(() => externalState) diff --git a/packages/table-core/tests/unit/core/tableAtoms.test.ts b/packages/table-core/tests/unit/core/tableAtoms.test.ts index 316519fb65..3f6930dab0 100644 --- a/packages/table-core/tests/unit/core/tableAtoms.test.ts +++ b/packages/table-core/tests/unit/core/tableAtoms.test.ts @@ -1,7 +1,9 @@ import { describe, expect, it, vi } from 'vitest' import { batch, createAtom } from '@tanstack/store' import { + columnFilteringFeature, constructTable, + globalFilteringFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, @@ -180,6 +182,71 @@ describe('three-layer atom architecture', () => { }) }) + describe('explicitly undefined controlled state (#5909)', () => { + it('falls back to the slice initial default at construct', () => { + const table = makeTable({ state: { sorting: undefined } }) + expect(table.atoms.sorting.get()).toEqual([]) + expect(table.store.state.sorting).toEqual([]) + expect(table.baseAtoms.sorting.get()).toEqual([]) + }) + + it('falls back to user-provided initialState', () => { + const table = makeTable({ + initialState: { sorting: [{ id: 'name', desc: true }] }, + state: { sorting: undefined }, + }) + expect(table.store.state.sorting).toEqual([{ id: 'name', desc: true }]) + }) + + it('does not poison baseAtoms when a controlled slice becomes undefined', () => { + const controlled: SortingState = [{ id: 'name', desc: false }] + const table = makeTable({ state: { sorting: controlled } }) + const internalTable = table as unknown as Table_Internal< + typeof features, + any + > + expect(table.baseAtoms.sorting.get()).toBe(controlled) + + table_setOptions(internalTable, (options) => ({ + ...options, + state: { sorting: undefined }, + })) + expect(table.store.state.sorting).toEqual([]) + expect(table.baseAtoms.sorting.get()).toEqual([]) + + // removing the key returns the slice to uncontrolled internal writes + table_setOptions(internalTable, (options) => ({ + ...options, + state: {}, + })) + table.setSorting([{ id: 'age', desc: true }]) + expect(table.store.state.sorting).toEqual([{ id: 'age', desc: true }]) + }) + + it('controlled globalFilter can still be cleared with undefined', () => { + const gfFeatures = testFeatures({ + columnFilteringFeature, + globalFilteringFeature, + }) + const table = constructTable({ + features: gfFeatures, + columns: [], + data: [], + state: { globalFilter: 'search' }, + }) + expect(table.store.state.globalFilter).toBe('search') + + table_setOptions( + table as unknown as Table_Internal, + (options) => ({ + ...options, + state: { globalFilter: undefined }, + }), + ) + expect(table.store.state.globalFilter).toBeUndefined() + }) + }) + describe('store (readonly flat derived)', () => { it('has identical public shape to TableState', () => { const table = makeTable()