diff --git a/.changeset/fix-svelte-store-proxy-equality.md b/.changeset/fix-svelte-store-proxy-equality.md new file mode 100644 index 00000000..358eeaf4 --- /dev/null +++ b/.changeset/fix-svelte-store-proxy-equality.md @@ -0,0 +1,5 @@ +--- +'@tanstack/svelte-store': patch +--- + +Fix `state_proxy_equality_mismatch` warning in `useSelector` by using `$state.raw()` instead of `$state()` for the slice variable. `$state()` wrapped object values in a Svelte Proxy, causing `===` comparison with the raw selector output to always fail, which triggered unnecessary re-renders and Svelte runtime warnings on every store update. diff --git a/packages/svelte-store/src/useSelector.svelte.ts b/packages/svelte-store/src/useSelector.svelte.ts index 82a72fb1..789c6722 100644 --- a/packages/svelte-store/src/useSelector.svelte.ts +++ b/packages/svelte-store/src/useSelector.svelte.ts @@ -35,7 +35,9 @@ export function useSelector>( options: UseSelectorOptions = {}, ): { readonly current: TSelected } { const compare = options.compare ?? defaultCompare - let slice = $state(selector(source.get())) + // `$state.raw` keeps the slice unproxied; a proxied value would never be `===` + // to the plain object the selector returns, defeating the equality check below. + let slice = $state.raw(selector(source.get())) $effect(() => { const unsub = source.subscribe((s) => { diff --git a/packages/svelte-store/tests/ProxyEquality.test.svelte b/packages/svelte-store/tests/ProxyEquality.test.svelte new file mode 100644 index 00000000..df577d85 --- /dev/null +++ b/packages/svelte-store/tests/ProxyEquality.test.svelte @@ -0,0 +1,35 @@ + + +
+

Number rendered: {renderCount}

+

Value: {selected.current.value}

+ +
diff --git a/packages/svelte-store/tests/index.test.ts b/packages/svelte-store/tests/index.test.ts index dc304f01..792e7bd0 100644 --- a/packages/svelte-store/tests/index.test.ts +++ b/packages/svelte-store/tests/index.test.ts @@ -5,6 +5,7 @@ import { shallow } from '../src/index.svelte.js' import TestBaseStore from './BaseStore.test.svelte' import TestRerender from './Render.test.svelte' import TestValue from './Value.test.svelte' +import TestProxyEquality from './ProxyEquality.test.svelte' const user = userEvent.setup() @@ -28,6 +29,14 @@ describe('useSelector', () => { expect(getByText('Number rendered: 2')).toBeInTheDocument() }) + it('does not trigger re-render when selector returns same object reference', async () => { + const { getByText } = render(TestProxyEquality) + expect(getByText('Number rendered: 1')).toBeInTheDocument() + + await user.click(getByText('Update ignored')) + expect(getByText('Number rendered: 1')).toBeInTheDocument() + }) + it('useSelector reads writable and readonly store state', async () => { const { getByText } = render(TestValue) expect(getByText('Value: 1')).toBeInTheDocument()