Skip to content

Commit c4b32c2

Browse files
fix: untrack $inspect.with and add check for unsafe mutation (#16209)
Co-authored-by: Rich Harris <[email protected]>
1 parent 4db4ee5 commit c4b32c2

File tree

10 files changed

+70
-7
lines changed

10 files changed

+70
-7
lines changed

.changeset/clever-dodos-jam.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: untrack `$inspect.with` and add check for unsafe mutation

documentation/docs/98-reference/.generated/client-errors.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ Cannot set prototype of `$state` object
125125
### state_unsafe_mutation
126126

127127
```
128-
Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
128+
Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
129129
```
130130

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

packages/svelte/messages/client-errors/errors.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
8282
8383
## state_unsafe_mutation
8484

85-
> Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
85+
> Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
8686
8787
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:
8888

packages/svelte/src/internal/client/dev/inspect.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { UNINITIALIZED } from '../../../constants.js';
22
import { snapshot } from '../../shared/clone.js';
33
import { inspect_effect, validate_effect } from '../reactivity/effects.js';
4+
import { untrack } from '../runtime.js';
45

56
/**
67
* @param {() => any[]} get_value
@@ -28,7 +29,10 @@ export function inspect(get_value, inspector = console.log) {
2829
}
2930

3031
if (value !== UNINITIALIZED) {
31-
inspector(initial ? 'init' : 'update', ...snapshot(value, true));
32+
var snap = snapshot(value, true);
33+
untrack(() => {
34+
inspector(initial ? 'init' : 'update', ...snap);
35+
});
3236
}
3337

3438
initial = false;

packages/svelte/src/internal/client/errors.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,12 +307,12 @@ export function state_prototype_fixed() {
307307
}
308308

309309
/**
310-
* Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
310+
* Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
311311
* @returns {never}
312312
*/
313313
export function state_unsafe_mutation() {
314314
if (DEV) {
315-
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`);
315+
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`);
316316

317317
error.name = 'Svelte error';
318318
throw error;

packages/svelte/src/internal/client/reactivity/sources.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,11 @@ export function mutate(source, value) {
135135
export function set(source, value, should_proxy = false) {
136136
if (
137137
active_reaction !== null &&
138-
!untracking &&
138+
// since we are untracking the function inside `$inspect.with` we need to add this check
139+
// to ensure we error if state is set inside an inspect effect
140+
(!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) &&
139141
is_runes() &&
140-
(active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 &&
142+
(active_reaction.f & (DERIVED | BLOCK_EFFECT | INSPECT_EFFECT)) !== 0 &&
141143
!(reaction_sources?.[1].includes(source) && reaction_sources[0] === active_reaction)
142144
) {
143145
e.state_unsafe_mutation();
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
compileOptions: {
6+
dev: true
7+
},
8+
error: 'state_unsafe_mutation'
9+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
let a = $state(0);
3+
4+
let b = $state(0);
5+
6+
$inspect(a).with((...args)=>{
7+
console.log(...args);
8+
b++;
9+
});
10+
</script>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
compileOptions: {
6+
dev: true
7+
},
8+
async test({ assert, target, logs }) {
9+
const [a, b] = target.querySelectorAll('button');
10+
assert.deepEqual(logs, ['init', 0]);
11+
flushSync(() => {
12+
b?.click();
13+
});
14+
assert.deepEqual(logs, ['init', 0]);
15+
flushSync(() => {
16+
a?.click();
17+
});
18+
assert.deepEqual(logs, ['init', 0, 'update', 1]);
19+
}
20+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
let a = $state(0);
3+
4+
let b = $state(0);
5+
6+
$inspect(a).with((...args)=>{
7+
console.log(...args);
8+
b;
9+
});
10+
</script>
11+
12+
<button onclick={()=>a++}></button>
13+
<button onclick={()=>b++}></button>

0 commit comments

Comments
 (0)