Skip to content

test(svelte-store): pin useSelector memoization - #1

Merged
seongwon030 merged 1 commit into
seongwon030:fix/svelte-store-use-selector-proxy-equalityfrom
jizc:test/selector-memoization-harness
Jul 27, 2026
Merged

test(svelte-store): pin useSelector memoization#1
seongwon030 merged 1 commit into
seongwon030:fix/svelte-store-use-selector-proxy-equalityfrom
jizc:test/selector-memoization-harness

Conversation

@jizc

@jizc jizc commented Jul 26, 2026

Copy link
Copy Markdown

The harness offered in TanStack#334, ported into packages/svelte-store/tests as you asked. One commit on top of fix/svelte-store-use-selector-proxy-equality, tests only — no source or changeset changes, since your commit already covers both.

What it adds

A controlled measurement to sit beside the regression test already on the branch. ProxyEquality.test.svelte establishes that an unrelated update stops re-rendering; this establishes how much was being paid and why, so a future regression reports a diagnosis rather than a bare count.

Three arms share one button, so a single click measures all of them under identical conditions:

Arm Slice Compare
default object the library's own ===
keyed object a.id === b.id — a compare no proxy can fool (control)
primitive string never proxied by Svelte (control)

Only the default arm regresses when the slice is proxied. That is what pins the cause to the proxying rather than to the store or the subscription — the controls are the point, not padding.

Red/green

Reverting $state.raw back to $state on this branch:

$state.raw $state
Default renders / identities 1 / 1 4 / 4
Same object true false
Keyed renders 1 1
Primitive renders 1 1

17 pass on the branch; 3 fail when reverted. Because every counter renders into the DOM, the failure output prints the whole experiment at once:

Default renders: 4
Default identities: 4
Same object: false
Keyed renders: 1
Primitive renders: 1

Three details worth flagging

Identities are counted, not just renders. Re-setting the slice mints a brand-new Proxy per notification — proxy() has no per-target cache — so source.equals(new_proxy) is false and every dependent re-runs. Counting distinct identities separates "notified again" from "notified with something genuinely new".

The updates are clicked one at a time. Svelte coalesces a synchronous burst into a single effect run, which would hide a cost paid once per notification.

One test asserts no state_proxy_equality_mismatch is emitted. Svelte's instrumented === warns only when (a === b) !== (get_proxied_value(a) === get_proxied_value(b)) — that is, only when both operands are one underlying object yet === disagrees. It therefore fires precisely when the slice is wrapped, which makes it a tight guard against a revert to $state(...).

Conventions

Written to match the existing suite: jsdom + @testing-library/svelte, counters rendered into the DOM, one props-free component per scenario. No type annotations in the .svelte file — eslint-plugin-svelte is not configured with the TS parser here, so any annotation is a parse error, which is presumably why no existing component carries one.

One eslint-disable for svelte/prefer-svelte-reactivity: the identity set is deliberately a plain Set, because a SvelteSet would pull the instrument into the reactivity it is measuring. The reason is in a comment above it.

Verified clean: vitest 17 passed, eslint 0 problems, svelte-check 0 errors / 0 warnings, prettier --check clean, no Svelte compiler warnings.

Happy to adjust naming, drop the warning assertion, or split the arms into separate components if you would rather they match the one-file-per-scenario shape more literally.

Adds a controlled measurement of whether useSelector memoizes an
unchanged slice, alongside the regression test already on this branch.

Three arms share one button, so a failure says why it failed: an object
slice on the library's own compare, the same slice behind a compare no
proxy can fool, and a primitive slice Svelte never proxies. Only the
first regresses when the slice is proxied, which pins the cause to the
proxying rather than to the store or the subscription. Reverting
$state.raw to $state reads 4 renders over 4 distinct identities, against
1 for either control.

Identities are counted rather than only renders: re-setting the slice
mints a brand-new Proxy per notification, so the count separates
"notified again" from "notified with something genuinely new".

The updates are clicked one at a time. Svelte coalesces a synchronous
burst into a single effect run, which would hide a cost paid once per
notification.

One test asserts no state_proxy_equality_mismatch is emitted. Svelte
warns there only when both operands are the same underlying object yet
=== disagrees, so it fires precisely when the slice is wrapped.
@seongwon030 seongwon030 assigned jizc and unassigned jizc Jul 27, 2026
@seongwon030
seongwon030 merged commit dd5da7b into seongwon030:fix/svelte-store-use-selector-proxy-equality Jul 27, 2026
@jizc
jizc deleted the test/selector-memoization-harness branch July 27, 2026 09:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants