Skip to content

Commit 050f9b9

Browse files
committed
fix(svelte-store): use $state.raw in useSelector to prevent proxy equality mismatch
Closes #322
1 parent 86251e5 commit 050f9b9

4 files changed

Lines changed: 50 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/svelte-store': patch
3+
---
4+
5+
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.

packages/svelte-store/src/useSelector.svelte.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function useSelector<TState, TSelected = NoInfer<TState>>(
3535
options: UseSelectorOptions<TSelected> = {},
3636
): { readonly current: TSelected } {
3737
const compare = options.compare ?? defaultCompare
38-
let slice = $state(selector(source.get()))
38+
let slice = $state.raw(selector(source.get()))
3939

4040
$effect(() => {
4141
const unsub = source.subscribe((s) => {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script lang="ts">
2+
import { untrack } from 'svelte'
3+
import { createStore } from '@tanstack/store'
4+
import { useSelector } from '../src/index.svelte.js'
5+
6+
const store = createStore({
7+
selected: { value: 1 },
8+
ignored: 0,
9+
})
10+
11+
const selected = useSelector(store, (state) => state.selected)
12+
13+
let renderCount = $state(0)
14+
15+
$effect(() => {
16+
selected.current
17+
untrack(() => {
18+
renderCount++
19+
})
20+
})
21+
</script>
22+
23+
<div>
24+
<p>Number rendered: {renderCount}</p>
25+
<p>Value: {selected.current.value}</p>
26+
<button
27+
onclick={() =>
28+
store.setState((v) => ({
29+
...v,
30+
ignored: v.ignored + 1,
31+
}))}
32+
>
33+
Update ignored
34+
</button>
35+
</div>

packages/svelte-store/tests/index.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { shallow } from '../src/index.svelte.js'
55
import TestBaseStore from './BaseStore.test.svelte'
66
import TestRerender from './Render.test.svelte'
77
import TestValue from './Value.test.svelte'
8+
import TestProxyEquality from './ProxyEquality.test.svelte'
89

910
const user = userEvent.setup()
1011

@@ -28,6 +29,14 @@ describe('useSelector', () => {
2829
expect(getByText('Number rendered: 2')).toBeInTheDocument()
2930
})
3031

32+
it('does not trigger re-render when selector returns same object reference', async () => {
33+
const { getByText } = render(TestProxyEquality)
34+
expect(getByText('Number rendered: 1')).toBeInTheDocument()
35+
36+
await user.click(getByText('Update ignored'))
37+
expect(getByText('Number rendered: 1')).toBeInTheDocument()
38+
})
39+
3140
it('useSelector reads writable and readonly store state', async () => {
3241
const { getByText } = render(TestValue)
3342
expect(getByText('Value: 1')).toBeInTheDocument()

0 commit comments

Comments
 (0)