From ed802760ed16329cf8ad8acdaa48e58823741048 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway <dg@domgan.com> Date: Thu, 19 Dec 2024 23:40:09 +0000 Subject: [PATCH] fix: make untrack behave correctly in relation to mutations --- .changeset/strong-cows-jump.md | 5 +++ packages/svelte/src/index-client.js | 3 +- packages/svelte/src/index-server.js | 3 +- .../src/internal/client/reactivity/effects.js | 5 ++- .../src/internal/client/reactivity/sources.js | 4 +- .../svelte/src/internal/client/runtime.js | 37 +++++++++++++++++-- packages/svelte/src/store/utils.js | 4 +- .../samples/derived-map/main.svelte | 6 +-- .../each-block-default-arg/main.svelte | 4 +- .../samples/snippet-default-arg/main.svelte | 4 +- packages/svelte/types/index.d.ts | 7 ++++ 11 files changed, 64 insertions(+), 18 deletions(-) create mode 100644 .changeset/strong-cows-jump.md diff --git a/.changeset/strong-cows-jump.md b/.changeset/strong-cows-jump.md new file mode 100644 index 0000000000..600352e584 --- /dev/null +++ b/.changeset/strong-cows-jump.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: make untrack behave correctly in relation to mutations diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 587d766233..bd47c4398c 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -187,7 +187,8 @@ export { hasContext, setContext, tick, - untrack + untrack, + unsafe } from './internal/client/runtime.js'; export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index 0f1aff8f5a..9868120729 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -14,7 +14,8 @@ export { noop as afterUpdate, noop as onMount, noop as flushSync, - run as untrack + run as untrack, + run as unsafe } from './internal/shared/utils.js'; export function createEventDispatcher() { diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index bf890627f7..24b36ce90b 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -16,7 +16,8 @@ import { set_is_flushing_effect, set_signal_status, untrack, - skip_reaction + skip_reaction, + untracking } from '../runtime.js'; import { DIRTY, @@ -164,7 +165,7 @@ function create_effect(type, fn, sync, push = true) { * @returns {boolean} */ export function effect_tracking() { - if (active_reaction === null) { + if (active_reaction === null || untracking) { return false; } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 3e8c4a00c8..921151cfa9 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -18,7 +18,8 @@ import { set_derived_sources, check_dirtiness, set_is_flushing_effect, - is_flushing_effect + is_flushing_effect, + unsafe_mutations } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import { @@ -147,6 +148,7 @@ export function mutate(source, value) { export function set(source, value) { if ( active_reaction !== null && + !unsafe_mutations && is_runes() && (active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 && // If the source was created locally within the current derived, then diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 4a90a21971..9dd18a6948 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -78,6 +78,10 @@ let dev_effect_stack = []; /** @type {null | Reaction} */ export let active_reaction = null; +export let untracking = false; + +export let unsafe_mutations = false; + /** @param {null | Reaction} reaction */ export function set_active_reaction(reaction) { active_reaction = reaction; @@ -387,6 +391,8 @@ export function update_reaction(reaction) { var previous_skip_reaction = skip_reaction; var prev_derived_sources = derived_sources; var previous_component_context = component_context; + var previous_untracking = untracking; + var previous_unsafe_mutations = unsafe_mutations; var flags = reaction.f; new_deps = /** @type {null | Value[]} */ (null); @@ -396,6 +402,8 @@ export function update_reaction(reaction) { skip_reaction = !is_flushing_effect && (flags & UNOWNED) !== 0; derived_sources = null; component_context = reaction.ctx; + untracking = false; + unsafe_mutations = false; try { var result = /** @type {Function} */ (0, reaction.fn)(); @@ -434,6 +442,8 @@ export function update_reaction(reaction) { skip_reaction = previous_skip_reaction; derived_sources = prev_derived_sources; component_context = previous_component_context; + untracking = previous_untracking; + unsafe_mutations = previous_unsafe_mutations; } } @@ -856,7 +866,7 @@ export function get(signal) { } // Register the dependency on the current reaction signal. - if (active_reaction !== null) { + if (active_reaction !== null && !untracking) { if (derived_sources !== null && derived_sources.includes(signal)) { e.state_unsafe_local_read(); } @@ -1016,12 +1026,31 @@ export function invalidate_inner_signals(fn) { * @returns {T} */ export function untrack(fn) { - const previous_reaction = active_reaction; + var previous_untracking = untracking; try { - active_reaction = null; + untracking = true; return fn(); } finally { - active_reaction = previous_reaction; + untracking = previous_untracking; + } +} + +/** + * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived), + * any state updates to state is allowed. + * + * ``` + * @template T + * @param {() => T} fn + * @returns {T} + */ +export function unsafe(fn) { + var previous_unsafe_mutations = unsafe_mutations; + try { + unsafe_mutations = true; + return fn(); + } finally { + unsafe_mutations = previous_unsafe_mutations; } } diff --git a/packages/svelte/src/store/utils.js b/packages/svelte/src/store/utils.js index db2a62c68c..4cdd49a002 100644 --- a/packages/svelte/src/store/utils.js +++ b/packages/svelte/src/store/utils.js @@ -1,5 +1,5 @@ /** @import { Readable } from './public' */ -import { untrack } from '../index-client.js'; +import { unsafe } from '../index-client.js'; import { noop } from '../internal/shared/utils.js'; /** @@ -22,7 +22,7 @@ export function subscribe_to_store(store, run, invalidate) { // Svelte store takes a private second argument // StartStopNotifier could mutate state, and we want to silence the corresponding validation error - const unsub = untrack(() => + const unsub = unsafe(() => store.subscribe( run, // @ts-expect-error diff --git a/packages/svelte/tests/runtime-runes/samples/derived-map/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-map/main.svelte index ea51f29dfb..e5f3798948 100644 --- a/packages/svelte/tests/runtime-runes/samples/derived-map/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/derived-map/main.svelte @@ -1,5 +1,5 @@ <script> - import { untrack } from 'svelte'; + import { unsafe } from 'svelte'; import { SvelteMap } from 'svelte/reactivity'; const cache = new SvelteMap(); @@ -13,7 +13,7 @@ cache.set(id, id.toString()); }).then(() => cache.get(id)); - untrack(() => { + unsafe(() => { cache.set(id, promise); }); @@ -25,7 +25,7 @@ const value = $derived(get_async(1)); const value2 = $derived(get_async(1)); - // both values are read before the set + // both values are read before the set value; value2; </script> diff --git a/packages/svelte/tests/runtime-runes/samples/each-block-default-arg/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-block-default-arg/main.svelte index b9e77c4795..76e287f28d 100644 --- a/packages/svelte/tests/runtime-runes/samples/each-block-default-arg/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/each-block-default-arg/main.svelte @@ -1,9 +1,9 @@ <script> - import { untrack } from 'svelte'; + import { unsafe, untrack } from 'svelte'; let count = $state(0); function default_arg() { - untrack(() => count++); + untrack(() => unsafe(() => count++)); return 1; } </script> diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-default-arg/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-default-arg/main.svelte index dc39503e3a..3bd205b4c7 100644 --- a/packages/svelte/tests/runtime-runes/samples/snippet-default-arg/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/snippet-default-arg/main.svelte @@ -1,9 +1,9 @@ <script> - import { untrack } from 'svelte'; + import { unsafe, untrack } from 'svelte'; let count = $state(0); function default_arg() { - untrack(() => count++); + untrack(() => unsafe(() => count++)); return 1; } </script> diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index d422abebbc..68cf4fde8b 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -484,6 +484,13 @@ declare module 'svelte' { * ``` * */ export function untrack<T>(fn: () => T): T; + /** + * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived), + * any state updates to state is allowed. + * + * ``` + * */ + export function unsafe<T>(fn: () => T): T; /** * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation.