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 <simon.holthausen@vercel.com>
main
Minh Vu 2 days ago committed by GitHub
parent 28b99aa8e6
commit 020242d6be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: restore hydration state when custom element attribute updates throw

@ -298,6 +298,7 @@ function set_attributes(
var is_option_element = element.nodeName === OPTION_TAG; var is_option_element = element.nodeName === OPTION_TAG;
var is_select_element = element.nodeName === SELECT_TAG; var is_select_element = element.nodeName === SELECT_TAG;
try {
for (var key in prev) { for (var key in prev) {
// don't null our internal $$onX listeners // don't null our internal $$onX listeners
if (!(key in next) && key[0] + key[1] !== '$$') { if (!(key in next) && key[0] + key[1] !== '$$') {
@ -317,7 +318,11 @@ function set_attributes(
var setters = get_setters(element); var setters = get_setters(element);
if (element.nodeName === INPUT_TAG && 'type' in next && ('value' in next || '__value' in next)) { if (
element.nodeName === INPUT_TAG &&
'type' in next &&
('value' in next || '__value' in next)
) {
var type = next.type; var type = next.type;
if (type !== current.type || (type === undefined && element.hasAttribute('type'))) { if (type !== current.type || (type === undefined && element.hasAttribute('type'))) {
@ -485,10 +490,11 @@ function set_attributes(
} }
} }
} }
} finally {
if (is_hydrating_custom_element) { if (is_hydrating_custom_element) {
set_hydrating(true); set_hydrating(true);
} }
}
return current; return current;
} }

@ -0,0 +1,11 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['hydrate'],
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, '<p>failed: setter error</p><p>after</p>');
}
});

@ -0,0 +1,22 @@
<script module>
if (!customElements.get('throwing-element')) {
customElements.define(
'throwing-element',
class extends HTMLElement {
set value(_) {
throw new Error('setter error');
}
}
);
}
</script>
<svelte:boundary>
<throwing-element {...{ value: 'boom' }}></throwing-element>
{#snippet failed(error)}
<p>failed: {error.message}</p>
{/snippet}
</svelte:boundary>
<p>after</p>
Loading…
Cancel
Save