From 020242d6bef059df9ae8c13dc8dbff4c9b31e0ff Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Mon, 28 Sep 2026 23:31:46 +0200 Subject: [PATCH] fix: ensure hydration state restoration for custom element attribute updates (#18467) ### Problem While hydrating custom elements, set_attributes temporarily disables global hydration state via set_hydrating(false) and restores it only at the end. If a prop setter or attribute operation throws partway through, hydration mode can remain disabled globally. ### Fix Wrap the temporary hydration-state override in a try/finally block in set_attributes so set_hydrating(true) always runs when is_hydrating_custom_element is true. --------- Co-authored-by: Simon Holthausen --- ...hydration-custom-element-error-boundary.md | 5 + .../client/dom/elements/attributes.js | 330 +++++++++--------- .../_config.js | 11 + .../main.svelte | 22 ++ 4 files changed, 206 insertions(+), 162 deletions(-) create mode 100644 .changeset/hydration-custom-element-error-boundary.md create mode 100644 packages/svelte/tests/runtime-runes/samples/hydration-custom-element-error-boundary/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/hydration-custom-element-error-boundary/main.svelte diff --git a/.changeset/hydration-custom-element-error-boundary.md b/.changeset/hydration-custom-element-error-boundary.md new file mode 100644 index 0000000000..f94aa6265a --- /dev/null +++ b/.changeset/hydration-custom-element-error-boundary.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: restore hydration state when custom element attribute updates throw diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 7d2dd62baf..982a405f98 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -298,196 +298,202 @@ function set_attributes( var is_option_element = element.nodeName === OPTION_TAG; var is_select_element = element.nodeName === SELECT_TAG; - for (var key in prev) { - // don't null our internal $$onX listeners - if (!(key in next) && key[0] + key[1] !== '$$') { - next[key] = null; - } - } - - if (next.class) { - next.class = clsx(next.class); - } else if (css_hash || next[CLASS]) { - next.class = null; /* force call to set_class() */ - } - - if (next[STYLE]) { - next.style ??= null; /* force call to set_style() */ - } - - var setters = get_setters(element); - - if (element.nodeName === INPUT_TAG && 'type' in next && ('value' in next || '__value' in next)) { - var type = next.type; - - if (type !== current.type || (type === undefined && element.hasAttribute('type'))) { - current.type = type; - set_attribute(element, 'type', type, skip_warning); + try { + for (var key in prev) { + // don't null our internal $$onX listeners + if (!(key in next) && key[0] + key[1] !== '$$') { + next[key] = null; + } } - } - // since key is captured we use const - for (const key in next) { - // let instead of var because referenced in a closure - let value = next[key]; - - // Up here because we want to do this for the initial value, too, even if it's undefined, - // and this wouldn't be reached in case of undefined because of the equality check below - if (is_option_element && key === 'value' && value == null) { - // The