diff --git a/.changeset/quick-meals-shout.md b/.changeset/quick-meals-shout.md new file mode 100644 index 0000000000..21871edd67 --- /dev/null +++ b/.changeset/quick-meals-shout.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't notify `searchParams` subscribers when the URL changes without affecting the search string diff --git a/packages/svelte/src/reactivity/url-search-params.js b/packages/svelte/src/reactivity/url-search-params.js index 38cf3ebe4f..d745bb6b08 100644 --- a/packages/svelte/src/reactivity/url-search-params.js +++ b/packages/svelte/src/reactivity/url-search-params.js @@ -54,6 +54,11 @@ export class SvelteURLSearchParams extends URLSearchParams { */ [REPLACE](params) { if (this.#updating) return; + + // the URL may have changed in a way that leaves the search string untouched — + // don't rebuild the params or notify readers if nothing changed + if (params.toString() === super.toString()) return; + this.#updating = true; for (const key of [...super.keys()]) { @@ -126,6 +131,16 @@ export class SvelteURLSearchParams extends URLSearchParams { return super.keys(); } + /** + * @param {(value: string, key: string, parent: URLSearchParams) => void} callback + * @param {any} [this_arg] + * @returns {void} + */ + forEach(callback, this_arg) { + get(this.#version); + super.forEach(callback, this_arg); + } + /** * @param {string} name * @param {string} value diff --git a/packages/svelte/src/reactivity/url-search-params.test.ts b/packages/svelte/src/reactivity/url-search-params.test.ts index b0c84872b0..765b4020f3 100644 --- a/packages/svelte/src/reactivity/url-search-params.test.ts +++ b/packages/svelte/src/reactivity/url-search-params.test.ts @@ -244,3 +244,24 @@ test('URLSearchParams.toString', () => { test('SvelteURLSearchParams instanceof URLSearchParams', () => { assert.ok(new SvelteURLSearchParams() instanceof URLSearchParams); }); + +test('params.forEach is reactive', () => { + const params = new SvelteURLSearchParams('foo=1'); + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + const entries: string[] = []; + params.forEach((value, key) => entries.push(`${key}=${value}`)); + log.push(entries.join('&')); + }); + }); + + flushSync(() => { + params.set('foo', '2'); + }); + + assert.deepEqual(log, ['foo=1', 'foo=2']); + + cleanup(); +}); diff --git a/packages/svelte/src/reactivity/url.test.ts b/packages/svelte/src/reactivity/url.test.ts index a79aa32315..d698116421 100644 --- a/packages/svelte/src/reactivity/url.test.ts +++ b/packages/svelte/src/reactivity/url.test.ts @@ -166,3 +166,77 @@ test('url.search normalizes value', () => { test('SvelteURL instanceof URL', () => { assert.ok(new SvelteURL('https://svelte.dev') instanceof URL); }); + +test('url.searchParams subscribers are not notified by changes that leave the search string untouched', () => { + const url = new SvelteURL('https://svelte.dev/a?foo=bar'); + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + log.push(url.searchParams.toString()); + }); + }); + + flushSync(() => { + // does not affect the search string + url.pathname = '/b'; + }); + + flushSync(() => { + // neither does this + url.href = 'https://svelte.dev/c?foo=bar#hash'; + }); + + flushSync(() => { + // but this does + url.search = '?foo=baz'; + }); + + assert.deepEqual(log, ['foo=bar', 'foo=baz']); + + cleanup(); +}); + +test('url.searchParams.size is not notified by unrelated href changes', () => { + const url = new SvelteURL('https://svelte.dev/?foo=bar'); + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + log.push(url.searchParams.size); + }); + }); + + flushSync(() => { + url.href = 'https://svelte.dev/other?foo=bar'; + }); + + flushSync(() => { + url.searchParams.append('baz', 'qux'); + }); + + assert.deepEqual(log, [1, 2]); + + cleanup(); +}); + +test('url.searchParams.forEach re-runs when the search string changes via the URL', () => { + const url = new SvelteURL('https://svelte.dev/?foo=1'); + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + const entries: string[] = []; + url.searchParams.forEach((value, key) => entries.push(`${key}=${value}`)); + log.push(entries.join('&')); + }); + }); + + flushSync(() => { + url.href = 'https://svelte.dev/?bar=2'; + }); + + assert.deepEqual(log, ['foo=1', 'bar=2']); + + cleanup(); +});