From 15141a83a276d2111326c6f370d222ac0c7a670e Mon Sep 17 00:00:00 2001 From: Nick Spagnola Date: Fri, 18 Sep 2026 23:43:22 +0100 Subject: [PATCH 1/2] fix(svelte-table): keep controlled state from resetting after each change The options store was a deep `$state`, so every options sync wrapped the caller's `data` array in a new proxy. The core row model memoizes on `table.options.data`, saw new data, and ran its auto-resets, undoing the controlled `expanded` or `pagination` change that caused the sync. `set` always replaces the whole options object, so `$state.raw` gives the store all the reactivity it needs. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/raw-svelte-options-store.md | 5 +++ .../svelte-table/src/reactivity.svelte.ts | 2 +- .../tests/adapter-lifecycle.test.ts | 21 ++++++++++ .../tests/fixtures/ExpandingHarness.svelte | 42 +++++++++++++++++++ .../tests/fixtures/PaginationHarness.svelte | 1 + 5 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 .changeset/raw-svelte-options-store.md create mode 100644 packages/svelte-table/tests/fixtures/ExpandingHarness.svelte 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..c360b22830 100644 --- a/packages/svelte-table/src/reactivity.svelte.ts +++ b/packages/svelte-table/src/reactivity.svelte.ts @@ -32,7 +32,7 @@ function subscribeToRune( } 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(',')} + From b25c4dce1d140a9fae43fd659e5dd27d5423b9a6 Mon Sep 17 00:00:00 2001 From: Nick Spagnola Date: Sat, 24 Oct 2026 20:08:28 -0400 Subject: [PATCH 2/2] docs(svelte-table): explain why the options store uses $state.raw Co-Authored-By: Claude Opus 5.5 --- packages/svelte-table/src/reactivity.svelte.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/svelte-table/src/reactivity.svelte.ts b/packages/svelte-table/src/reactivity.svelte.ts index c360b22830..93697c5e49 100644 --- a/packages/svelte-table/src/reactivity.svelte.ts +++ b/packages/svelte-table/src/reactivity.svelte.ts @@ -31,6 +31,12 @@ 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.raw(initialValue)