From 320c36777e5d57d5cff08df5233b3c05184ec201 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Fri, 17 Jul 2026 15:28:26 -0500 Subject: [PATCH] fix controlled state handling for undefined options --- .../src/core/table/constructTable.ts | 13 ++++++++++++ .../src/core/table/coreTablesFeature.utils.ts | 19 ++++++++++++++---- .../tests/unit/core/tableAtoms.test.ts | 20 +++++++++++++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/packages/table-core/src/core/table/constructTable.ts b/packages/table-core/src/core/table/constructTable.ts index 3a2f648966..dfef1798f7 100644 --- a/packages/table-core/src/core/table/constructTable.ts +++ b/packages/table-core/src/core/table/constructTable.ts @@ -74,6 +74,19 @@ export function constructTable< const mergedOptions = { ...defaultOptions, ...tableOptions } + // Optional options passed through wrapper components commonly arrive as + // explicit `undefined` values. Treat those the same as omitted options so + // they do not replace feature defaults such as state updater callbacks. + for (const key in tableOptions) { + if ( + tableOptions[key as keyof typeof tableOptions] === undefined && + defaultOptions[key as keyof typeof defaultOptions] !== undefined + ) { + ;(mergedOptions as Record)[key] = + defaultOptions[key as keyof typeof defaultOptions] + } + } + if (_reactivity.wrapExternalAtoms && mergedOptions.atoms) { for (const [atomKey, _atom] of Object.entries(mergedOptions.atoms)) { const atom = _atom as Atom diff --git a/packages/table-core/src/core/table/coreTablesFeature.utils.ts b/packages/table-core/src/core/table/coreTablesFeature.utils.ts index 5003aa6cd4..f97d54ceff 100644 --- a/packages/table-core/src/core/table/coreTablesFeature.utils.ts +++ b/packages/table-core/src/core/table/coreTablesFeature.utils.ts @@ -26,12 +26,16 @@ export function table_syncExternalStateToBaseAtoms< table._reactivity.batch(() => { for (const key in state) { + const externalState = state[key as keyof typeof state] + if (externalState === undefined) { + continue + } + const baseAtom = (table.baseAtoms as Record)[key] if (!baseAtom) { continue } - const externalState = state[key as keyof typeof state] if (externalState !== table._reactivity.untrack(() => baseAtom.get())) { baseAtom.set(() => externalState) } @@ -94,13 +98,20 @@ export function table_mergeOptions< // simple merge if no mergeOptions is provided - performant if (!table.options.mergeOptions) { + const mergedOptions = { ...table.options } + for (const key in newOptions) { + const value = newOptions[key as keyof typeof newOptions] + if (value !== undefined) { + ;(mergedOptions as Record)[key] = value + } + } + return { - ...table.options, - ...newOptions, + ...mergedOptions, features, atoms, initialState, - } + } as TableOptions } // else use the mergeOptions function and preserve getters/setters diff --git a/packages/table-core/tests/unit/core/tableAtoms.test.ts b/packages/table-core/tests/unit/core/tableAtoms.test.ts index 441be055f4..5405c43407 100644 --- a/packages/table-core/tests/unit/core/tableAtoms.test.ts +++ b/packages/table-core/tests/unit/core/tableAtoms.test.ts @@ -68,6 +68,26 @@ describe('three-layer atom architecture', () => { expect(table.store.state.sorting).toEqual(external) }) + it('treats undefined controlled state and callbacks as omitted', () => { + const table = makeTable({ + // Simulate optional wrapper props passed through without conditionally + // removing their keys. + state: { sorting: undefined } as unknown as { sorting: SortingState }, + onSortingChange: undefined, + }) + + expect(table.atoms.sorting.get()).toEqual([]) + + table.setOptions((old) => ({ + ...old, + state: { sorting: undefined }, + onSortingChange: undefined, + })) + table.setSorting([{ id: 'name', desc: false }]) + + expect(table.atoms.sorting.get()).toEqual([{ id: 'name', desc: false }]) + }) + it('options.atoms[key] takes precedence over options.state[key] when both are present', () => { const externalAtom = createAtom([ { id: 'fromAtom', desc: true },