fix: untrack `$inspect.with` and add check for unsafe mutation (#16209)

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/16219/head
Paolo Ricciuti 2 months ago committed by GitHub
parent 4db4ee5330
commit c4b32c2bff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: untrack `$inspect.with` and add check for unsafe mutation

@ -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:

@ -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:

@ -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;

@ -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;

@ -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();

@ -0,0 +1,9 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
error: 'state_unsafe_mutation'
});

@ -0,0 +1,10 @@
<script>
let a = $state(0);
let b = $state(0);
$inspect(a).with((...args)=>{
console.log(...args);
b++;
});
</script>

@ -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]);
}
});

@ -0,0 +1,13 @@
<script>
let a = $state(0);
let b = $state(0);
$inspect(a).with((...args)=>{
console.log(...args);
b;
});
</script>
<button onclick={()=>a++}></button>
<button onclick={()=>b++}></button>
Loading…
Cancel
Save