From c0987c09f816b5d7dcb5564d19c849f7e28b9e7a Mon Sep 17 00:00:00 2001
From: subotac <73706465+subotac@users.noreply.github.com>
Date: Fri, 14 Aug 2026 23:50:35 +0300
Subject: [PATCH] fix: remove stale capture listeners from spread attributes
(#18618)
Fixes #18608.
When `set_attributes` normalized removed spread attributes, it also
copied its internal `$$` listener bookkeeping keys into `next` with a
`null` value. This cleared the stored callback before event cleanup, so
removing a capture handler passed `null` to `removeEventListener` and
left the listener attached.
Skip internal `$$` keys during missing-attribute normalization. The
updated runtime-runes sample verifies replacing and then removing a
spread capture handler in both client and hydration modes.
---------
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
---
.changeset/happy-spoons-talk.md | 5 +++++
.../client/dom/elements/attributes.js | 3 ++-
.../event-attribute-spread-update/_config.js | 21 ++++++++++++++++---
.../event-attribute-spread-update/main.svelte | 3 +++
4 files changed, 28 insertions(+), 4 deletions(-)
create mode 100644 .changeset/happy-spoons-talk.md
diff --git a/.changeset/happy-spoons-talk.md b/.changeset/happy-spoons-talk.md
new file mode 100644
index 0000000000..cf165320db
--- /dev/null
+++ b/.changeset/happy-spoons-talk.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: clean up removed capture event handlers from spread attributes
diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js
index 589685b5f2..8a25be7acd 100644
--- a/packages/svelte/src/internal/client/dom/elements/attributes.js
+++ b/packages/svelte/src/internal/client/dom/elements/attributes.js
@@ -315,7 +315,8 @@ function set_attributes(
var is_option_element = element.nodeName === OPTION_TAG;
for (var key in prev) {
- if (!(key in next)) {
+ // don't null our internal $$onX listeners
+ if (!(key in next) && key[0] + key[1] !== '$$') {
next[key] = null;
}
}
diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js
index af03eed4c9..5fbd37c768 100644
--- a/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js
@@ -3,16 +3,31 @@ import { test } from '../../test';
export default test({
test({ assert, target }) {
- const [change, increment] = target.querySelectorAll('button');
+ const [change, remove, increment] = target.querySelectorAll('button');
increment.click();
flushSync();
- assert.htmlEqual(target.innerHTML, '');
+ assert.htmlEqual(
+ target.innerHTML,
+ ''
+ );
change.click();
flushSync();
increment.click();
flushSync();
- assert.htmlEqual(target.innerHTML, '');
+ assert.htmlEqual(
+ target.innerHTML,
+ ''
+ );
+
+ remove.click();
+ flushSync();
+ increment.click();
+ flushSync();
+ assert.htmlEqual(
+ target.innerHTML,
+ ''
+ );
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte
index 32d4b242cc..ee049e469c 100644
--- a/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte
@@ -24,4 +24,7 @@
>
change handlers
+