diff --git a/.changeset/raw-svelte-options-store.md b/.changeset/raw-svelte-options-store.md new file mode 100644 index 0000000000..58fe7a1ab9 --- /dev/null +++ b/.changeset/raw-svelte-options-store.md @@ -0,0 +1,5 @@ +--- +'@tanstack/svelte-table': patch +--- + +Fix controlled `expanded` and `pagination` state resetting straight after each change. The options store now uses `$state.raw`, so an options sync no longer wraps `data` in a new proxy, which the core row model treated as new data and answered by running its auto-resets. diff --git a/packages/svelte-table/src/reactivity.svelte.ts b/packages/svelte-table/src/reactivity.svelte.ts index a251a7452e..93697c5e49 100644 --- a/packages/svelte-table/src/reactivity.svelte.ts +++ b/packages/svelte-table/src/reactivity.svelte.ts @@ -31,8 +31,14 @@ function subscribeToRune( return { unsubscribe } } +/** + * Backs the options store. Uses `$state.raw` because `set` always replaces the + * whole options object: a deep `$state` would wrap `options.data` in a new proxy + * on every set, which the row model treats as new data and resets controlled + * pagination/expanded state (#6640). + */ function createRuneWritableAtom(initialValue: T): Atom { - let value = $state(initialValue) + let value = $state.raw(initialValue) return { set: (updater: T | ((prevVal: T) => T)) => { diff --git a/packages/svelte-table/tests/adapter-lifecycle.test.ts b/packages/svelte-table/tests/adapter-lifecycle.test.ts index ac6d03bae5..dc30ee79ec 100644 --- a/packages/svelte-table/tests/adapter-lifecycle.test.ts +++ b/packages/svelte-table/tests/adapter-lifecycle.test.ts @@ -4,6 +4,7 @@ import { describe, expect, test, vi } from 'vitest' import { act, fireEvent, render, screen } from '@testing-library/svelte' import { createAtom } from '@tanstack/svelte-store' import CallbackHarness from './fixtures/CallbackHarness.svelte' +import ExpandingHarness from './fixtures/ExpandingHarness.svelte' import PaginationHarness from './fixtures/PaginationHarness.svelte' import ReactivityHarness from './fixtures/ReactivityHarness.svelte' import RuneStateHarness from './fixtures/RuneStateHarness.svelte' @@ -144,6 +145,26 @@ describe('Svelte adapter lifecycle and reactive options', () => { expect(outputText('Paginated row ids')).toBe('0,1,2') }) + test('createTableState-controlled pagination is not reset by its own change', async () => { + render(PaginationHarness) + + await fireEvent.click(screen.getByRole('button', { name: 'Next page' })) + await act() + + expect(outputText('Paginated row ids')).toBe('5,6,7,8,9') + }) + + test('createTableState-controlled expanded state is not reset by its own change', async () => { + render(ExpandingHarness) + + await fireEvent.click( + screen.getByRole('button', { name: 'Expand first row' }), + ) + await act() + + expect(outputText('Expanded row ids')).toBe('1') + }) + test('native rune projections only rerun for their tracked dependency', async () => { const selectedRowCaptor = vi.fn<(selected: boolean) => void>() const selectionCaptor = vi.fn<(selection: RowSelectionState) => void>() diff --git a/packages/svelte-table/tests/fixtures/ExpandingHarness.svelte b/packages/svelte-table/tests/fixtures/ExpandingHarness.svelte new file mode 100644 index 0000000000..5e75d1eb3f --- /dev/null +++ b/packages/svelte-table/tests/fixtures/ExpandingHarness.svelte @@ -0,0 +1,42 @@ + + +{table + .getRowModel() + .rows.filter((row) => row.getIsExpanded()) + .map((row) => row.id) + .join(',')} + diff --git a/packages/svelte-table/tests/fixtures/PaginationHarness.svelte b/packages/svelte-table/tests/fixtures/PaginationHarness.svelte index d3103b9a69..7e27afe79a 100644 --- a/packages/svelte-table/tests/fixtures/PaginationHarness.svelte +++ b/packages/svelte-table/tests/fixtures/PaginationHarness.svelte @@ -43,3 +43,4 @@ .join(',')} +