diff --git a/.changeset/clever-dodos-jam.md b/.changeset/clever-dodos-jam.md new file mode 100644 index 0000000000..bdeb979184 --- /dev/null +++ b/.changeset/clever-dodos-jam.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: untrack `$inspect.with` and add check for unsafe mutation diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 32348bb781..111b0b8940 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -125,7 +125,7 @@ Cannot set prototype of `$state` object ### state_unsafe_mutation ``` -Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` +Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state` ``` This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go: diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index c4e68f8fee..6d96770eba 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -82,7 +82,7 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long ## state_unsafe_mutation -> Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` +> Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state` This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go: diff --git a/packages/svelte/src/internal/client/dev/inspect.js b/packages/svelte/src/internal/client/dev/inspect.js index e13ef470cf..e15c66901c 100644 --- a/packages/svelte/src/internal/client/dev/inspect.js +++ b/packages/svelte/src/internal/client/dev/inspect.js @@ -1,6 +1,7 @@ import { UNINITIALIZED } from '../../../constants.js'; import { snapshot } from '../../shared/clone.js'; import { inspect_effect, validate_effect } from '../reactivity/effects.js'; +import { untrack } from '../runtime.js'; /** * @param {() => any[]} get_value @@ -28,7 +29,10 @@ export function inspect(get_value, inspector = console.log) { } if (value !== UNINITIALIZED) { - inspector(initial ? 'init' : 'update', ...snapshot(value, true)); + var snap = snapshot(value, true); + untrack(() => { + inspector(initial ? 'init' : 'update', ...snap); + }); } initial = false; diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 429dd99da9..962593b48d 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -307,12 +307,12 @@ export function state_prototype_fixed() { } /** - * Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` + * Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state` * @returns {never} */ export function state_unsafe_mutation() { if (DEV) { - const error = new Error(`state_unsafe_mutation\nUpdating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without \`$state\`\nhttps://svelte.dev/e/state_unsafe_mutation`); + const error = new Error(`state_unsafe_mutation\nUpdating state inside \`$derived(...)\`, \`$inspect(...)\` or a template expression is forbidden. If the value should not be reactive, declare it without \`$state\`\nhttps://svelte.dev/e/state_unsafe_mutation`); error.name = 'Svelte error'; throw error; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 4959bc1abc..0db3530232 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -135,9 +135,11 @@ export function mutate(source, value) { export function set(source, value, should_proxy = false) { if ( active_reaction !== null && - !untracking && + // since we are untracking the function inside `$inspect.with` we need to add this check + // to ensure we error if state is set inside an inspect effect + (!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) && is_runes() && - (active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 && + (active_reaction.f & (DERIVED | BLOCK_EFFECT | INSPECT_EFFECT)) !== 0 && !(reaction_sources?.[1].includes(source) && reaction_sources[0] === active_reaction) ) { e.state_unsafe_mutation(); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/_config.js new file mode 100644 index 0000000000..7e8fcd2d48 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/_config.js @@ -0,0 +1,9 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + error: 'state_unsafe_mutation' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/main.svelte new file mode 100644 index 0000000000..3361087ff7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/main.svelte @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-with-untracked/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-with-untracked/_config.js new file mode 100644 index 0000000000..cdb242c416 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-with-untracked/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + async test({ assert, target, logs }) { + const [a, b] = target.querySelectorAll('button'); + assert.deepEqual(logs, ['init', 0]); + flushSync(() => { + b?.click(); + }); + assert.deepEqual(logs, ['init', 0]); + flushSync(() => { + a?.click(); + }); + assert.deepEqual(logs, ['init', 0, 'update', 1]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-with-untracked/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-with-untracked/main.svelte new file mode 100644 index 0000000000..5bcf2bd348 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-with-untracked/main.svelte @@ -0,0 +1,13 @@ + + + + \ No newline at end of file