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 @@
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 @@
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 @@
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(fn: () => T): T;
+ /**
+ * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived),
+ * any state updates to state is allowed.
+ *
+ * ```
+ * */
+ export function unsafe(fn: () => T): T;
/**
* Retrieves the context that belongs to the closest parent component with the specified `key`.
* Must be called during component initialisation.