diff --git a/.changeset/fix-svelte-map-undefined.md b/.changeset/fix-svelte-map-undefined.md new file mode 100644 index 0000000000..fa981c46f8 --- /dev/null +++ b/.changeset/fix-svelte-map-undefined.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: `SvelteMap` incorrectly handles keys with `undefined` values diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index 014b5e7c7c..48d06a05a7 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -98,8 +98,7 @@ export class SvelteMap extends Map { var s = sources.get(key); if (s === undefined) { - var ret = super.get(key); - if (ret !== undefined) { + if (super.has(key)) { s = this.#source(0); if (DEV) { @@ -134,8 +133,7 @@ export class SvelteMap extends Map { var s = sources.get(key); if (s === undefined) { - var ret = super.get(key); - if (ret !== undefined) { + if (super.has(key)) { s = this.#source(0); if (DEV) { @@ -202,8 +200,11 @@ export class SvelteMap extends Map { if (s !== undefined) { sources.delete(key); - set(this.#size, super.size); set(s, -1); + } + + if (res) { + set(this.#size, super.size); increment(this.#version); } diff --git a/packages/svelte/src/reactivity/map.test.ts b/packages/svelte/src/reactivity/map.test.ts index 2f9f064b42..8bb6f72f7b 100644 --- a/packages/svelte/src/reactivity/map.test.ts +++ b/packages/svelte/src/reactivity/map.test.ts @@ -207,6 +207,75 @@ test('map handling of undefined values', () => { cleanup(); }); +test('map.has() and map.get() with undefined values', () => { + const map = new SvelteMap([['foo', undefined]]); + + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + log.push('has', map.has('foo')); + }); + + render_effect(() => { + log.push('get', map.get('foo')); + }); + + flushSync(() => { + map.delete('foo'); + }); + + flushSync(() => { + map.set('bar', undefined); + }); + }); + + assert.deepEqual(log, [ + 'has', + true, + 'get', + undefined, + 'has', + false, + 'get', + undefined, + // set('bar') bumps version, causing has('foo')/get('foo') effects to re-run + 'has', + false, + 'get', + undefined + ]); + + assert.equal(map.has('bar'), true); + assert.equal(map.get('bar'), undefined); + + cleanup(); +}); + +test('map.delete() triggers size reactivity for keys without per-key sources', () => { + const map = new SvelteMap([ + [1, 'a'], + [2, 'b'] + ]); + + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + log.push(map.size); + }); + + // delete key 2 which was never individually read (no per-key source) + flushSync(() => { + map.delete(2); + }); + }); + + assert.deepEqual(log, [2, 1]); + + cleanup(); +}); + test('not invoking reactivity when value is not in the map after changes', () => { const map = new SvelteMap([[1, 1]]);