diff --git a/.changeset/hydrated-boundary-reset.md b/.changeset/hydrated-boundary-reset.md new file mode 100644 index 0000000000..799a57fd9b --- /dev/null +++ b/.changeset/hydrated-boundary-reset.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: call `onerror` and provide a working `reset` when hydrating a failed boundary diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 0c0903ff52..4f655a3695 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -199,17 +199,68 @@ export class Boundary { */ #hydrate_failed_content(error) { const failed = this.#props.failed; + const { reset, invoke_onerror } = this.#create_reset(error); + + // `onerror` may mutate state, which is disallowed while hydrating + queue_micro_task(invoke_onerror); + if (!failed) return; this.#failed_effect = branch(() => { failed( this.#anchor, () => error, - () => () => {} + () => reset ); }); } + /** + * Creates the `reset` function for a failed boundary, along with a function + * that invokes `onerror` with it (if provided) + * @param {unknown} error + * @returns {{ reset: () => void, invoke_onerror: () => void }} + */ + #create_reset(error) { + var did_reset = false; + var calling_on_error = false; + + const reset = () => { + if (did_reset) { + w.svelte_boundary_reset_noop(); + return; + } + + did_reset = true; + + if (calling_on_error) { + e.svelte_boundary_reset_onerror(); + } + + if (this.#failed_effect !== null) { + pause_effect(this.#failed_effect, () => { + this.#failed_effect = null; + }); + } + + this.#run(() => { + this.#render(); + }); + }; + + const invoke_onerror = () => { + try { + calling_on_error = true; + this.#props.onerror?.(error, reset); + calling_on_error = false; + } catch (err) { + invoke_error_boundary(err, this.#effect && this.#effect.parent); + } + }; + + return { reset, invoke_onerror }; + } + #hydrate_pending_content() { const pending = this.#props.pending; if (!pending) return; @@ -429,43 +480,13 @@ export class Boundary { set_hydrate_node(skip_nodes()); } - var onerror = this.#props.onerror; let failed = this.#props.failed; - var did_reset = false; - var calling_on_error = false; - - const reset = () => { - if (did_reset) { - w.svelte_boundary_reset_noop(); - return; - } - - did_reset = true; - - if (calling_on_error) { - e.svelte_boundary_reset_onerror(); - } - - if (this.#failed_effect !== null) { - pause_effect(this.#failed_effect, () => { - this.#failed_effect = null; - }); - } - - this.#run(() => { - this.#render(); - }); - }; /** @param {unknown} transformed_error */ const handle_error_result = (transformed_error) => { - try { - calling_on_error = true; - onerror?.(transformed_error, reset); - calling_on_error = false; - } catch (error) { - invoke_error_boundary(error, this.#effect && this.#effect.parent); - } + const { reset, invoke_onerror } = this.#create_reset(transformed_error); + + invoke_onerror(); if (failed) { this.#failed_effect = this.#run(() => { diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/_config.js new file mode 100644 index 0000000000..f0aef7e92f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/_config.js @@ -0,0 +1,19 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + ssrHtml: '

failed: error

', + transformError: () => 'error', + + test({ assert, target, logs }) { + // `onerror` is called upon hydration with the deserialized error + assert.deepEqual(logs, ['onerror: error']); + + const btn = target.querySelector('button'); + btn?.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, '

recovered

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/child.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/child.svelte new file mode 100644 index 0000000000..e93c7bb0fa --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/child.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/main.svelte new file mode 100644 index 0000000000..e84198473b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/main.svelte @@ -0,0 +1,29 @@ + + + { + console.log(`onerror: ${error}`); + reset_fn = reset; + }} +> + {#if recovered} +

recovered

+ {:else} + + {/if} + + {#snippet failed(error)} +

failed: {error}

+ {/snippet} +
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/_config.js new file mode 100644 index 0000000000..e5c52eb19c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/_config.js @@ -0,0 +1,16 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + ssrHtml: '

failed: error

', + transformError: () => 'error', + + test({ assert, target }) { + const btn = target.querySelector('button'); + btn?.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, '

recovered

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/child.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/child.svelte new file mode 100644 index 0000000000..e93c7bb0fa --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/child.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/main.svelte new file mode 100644 index 0000000000..1642a94e7a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/main.svelte @@ -0,0 +1,22 @@ + + + + {#if recovered} +

recovered

+ {:else} + + {/if} + + {#snippet failed(error, reset)} +

failed: {error}

+ + {/snippet} +