各种笔记 – Svelte pitfalls - the-dissidents/subtle GitHub Wiki
The state_proxy_equality_mismatch
message reads: "Reactive $state(...)
proxies and the values they proxy have different identities ..." But what does that mean? It means you just can't compare the reference equality between them. And note that objects and arrays are deeply reactive; consider:
let obj = {};
let obj_state = $state(obj);
let deep = $state({});
deep.value = obj;
console.log(deep.value == obj) // warning state_proxy_equality_mismatch; false
console.log($state.snapshot(deep.value) == obj) // false
console.log($state.snapshot(deep.value) == obj_state) // false
console.log($state.snapshot(deep.value) == $state.snapshot(obj_state)) // false
deep.value = obj_state;
console.log(deep.value == obj_state) // true
console.log($state.snapshot(deep.value) == obj) // false
console.log($state.snapshot(deep.value) == obj_state) // false
console.log($state.snapshot(deep.value) == $state.snapshot(obj_state)) // false
And $state.snapshot
doesn't help here at all: it won't preserve the reference. Actually, it creates a clone of the object within the state (see issue #15022).
So, I repeat, you have next to no way to compare reference equality between a nonreactive object and 1) a reactive state, 2) a field in a deeply reactive object or 3) a member in a reactive array -- in roughly increasing order of unintuitiveness. Consider:
let obj = {};
let reactive_array = $state([]);
reactive_array.push(obj);
console.log(reactive_array.indexOf(obj)); // state_proxy_equality_mismatch; -1
You can solve the problem only by making the object reactive first:
let obj_state = $state(obj);
reactive_array.push(obj_state);
console.log(reactive_array.indexOf(obj_state)); // 1
... or immediately get the (now proxified) object from the array once it is pushed:
reactive_array.push(obj);
obj = reactive_array.at(-1);
console.log(reactive_array.indexOf(obj)); // 2
... or use a SvelteSet instead, but that's not possible if item order matters.
It should really be better documented. I have a feeling that the Svelte documentations tend to focus on making everything look simple and easy to use, while unintuitive cases like this (which is likely where people will pull their hairs out in confusion) go without mention.
"An effect only depends on the values that it read the last time it ran."
Which means this -- the first version of the hook
method in PublicConfig
that I wrote -- won't work, because if when the effect runs for the first time this.#initialized
isn't true, it won't run again anymore:
hook<T>(track: () => T, action: (value: T) => void) {
this.onInitialized(() => action(track()));
$effect(() => {
if (!this.#initialized) return;
const value = track();
untrack(() => action(value));
});
}
As of writing (2025.4.23), there is a pull request to add a $state.onchange
rune which removes this issue, and it looks like it will replace virtually all use cases of $effect
in this project. I think it will be merged relatively soon.
Consider:
<script>
let state = $state({o: 1})
let obj = {x: state};
$inspect(obj.x);
$effect(() => {
obj.x;
console.log('obj.x changed');
});
</script>
<button onclick={() => obj.x.o = 2}>test</button>
Clicking the button will not trigger the $effect
. However, it will trigger $inspect
!
If you want $effect
to work correctly here, refer to the field as $state.snapshot(obj.x)
instead;
I don't know the explanation for this yet.