diff --git a/.changeset/loud-rabbits-call.md b/.changeset/loud-rabbits-call.md new file mode 100644 index 000000000000..564601f1affc --- /dev/null +++ b/.changeset/loud-rabbits-call.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure unresolved async deriveds return undefined instead of internal symbol diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 6f941c7ff231..37951e9d74fd 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -21,6 +21,7 @@ import { MANAGED_EFFECT } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; +import { UNINITIALIZED } from '../../../constants.js'; import { deferred, define_property } from '../../shared/utils.js'; import { active_effect, @@ -320,7 +321,7 @@ export class Batch { // Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get` if ((source.f & ERROR_VALUE) === 0) { this.current.set(source, source.v); - batch_values?.set(source, source.v); + batch_values?.set(source, source.v === UNINITIALIZED ? undefined : source.v); } } @@ -546,7 +547,7 @@ export class Batch { for (const [source, previous] of batch.previous) { if (!batch_values.has(source)) { - batch_values.set(source, previous); + batch_values.set(source, previous === UNINITIALIZED ? undefined : previous); } } } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 64c8409b8ffb..5fba2277df31 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -163,6 +163,10 @@ export function is_dirty(reaction) { } if (flags & DERIVED) { + // UNINITIALIZED deriveds need computation + if (/** @type {Derived} */ (reaction).v === UNINITIALIZED) { + return true; + } reaction.f &= ~WAS_MARKED; } @@ -642,6 +646,10 @@ export function get(signal) { throw signal.v; } + if (signal.v === UNINITIALIZED) { + return /** @type {V} */ (undefined); + } + return signal.v; } diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/Child.svelte new file mode 100644 index 000000000000..9b705f3064a7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/Child.svelte @@ -0,0 +1,18 @@ + + +
bar: {bar}
+baz: {baz}
+{#if qux} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/_config.js new file mode 100644 index 000000000000..ff77b1c51234 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/_config.js @@ -0,0 +1,25 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +// Ensure async derived remains reactive with associated effect and boundary with guard (#17271) +// +// Accounts for both UNINITIALIZED leaking from get and the baz derived remaining NaN +// due to having both an $effect and a boundary with an if in the same template +export default test({ + html: `Loading...
`, + + skip_no_async: true, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +bar: 1
+baz: 69
+ + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/main.svelte new file mode 100644 index 000000000000..9687e514000b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/main.svelte @@ -0,0 +1,11 @@ + + +Loading...
+ {/snippet} + +