Skip to content

Commit 2070c8a

Browse files
authored
breaking: disallow state mutations in logic block expression (#13625)
1 parent 7429854 commit 2070c8a

File tree

8 files changed

+51
-9
lines changed

8 files changed

+51
-9
lines changed

.changeset/eighty-dryers-pretend.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+
breaking: disallow state mutations in logic block expression

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,5 +125,5 @@ Reading state that was created inside the same derived is forbidden. Consider us
125125
### state_unsafe_mutation
126126

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,4 @@
8282
8383
## state_unsafe_mutation
8484

85-
> Updating state inside a derived is forbidden. If the value should not be reactive, declare it without `$state`
85+
> Updating state inside a derived or logic block expression is forbidden. If the value should not be reactive, declare it without `$state`

packages/svelte/src/internal/client/dom/blocks/each.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
pause_effect,
3232
resume_effect
3333
} from '../../reactivity/effects.js';
34-
import { source, mutable_source, set } from '../../reactivity/sources.js';
34+
import { source, mutable_source, internal_set } from '../../reactivity/sources.js';
3535
import { array_from, is_array } from '../../../shared/utils.js';
3636
import { INERT } from '../../constants.js';
3737
import { queue_micro_task } from '../task.js';
@@ -463,11 +463,11 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
463463
*/
464464
function update_item(item, value, index, type) {
465465
if ((type & EACH_ITEM_REACTIVE) !== 0) {
466-
set(item.v, value);
466+
internal_set(item.v, value);
467467
}
468468

469469
if ((type & EACH_INDEX_REACTIVE) !== 0) {
470-
set(/** @type {Value<number>} */ (item.i), index);
470+
internal_set(/** @type {Value<number>} */ (item.i), index);
471471
} else {
472472
item.i = index;
473473
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,12 +343,12 @@ export function state_unsafe_local_read() {
343343
}
344344

345345
/**
346-
* Updating state inside a derived is forbidden. If the value should not be reactive, declare it without `$state`
346+
* Updating state inside a derived or logic block expression is forbidden. If the value should not be reactive, declare it without `$state`
347347
* @returns {never}
348348
*/
349349
export function state_unsafe_mutation() {
350350
if (DEV) {
351-
const error = new Error(`state_unsafe_mutation\nUpdating state inside a derived is forbidden. If the value should not be reactive, declare it without \`$state\``);
351+
const error = new Error(`state_unsafe_mutation\nUpdating state inside a derived or logic block expression is forbidden. If the value should not be reactive, declare it without \`$state\``);
352352

353353
error.name = 'Svelte error';
354354
throw error;

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ import {
2828
BRANCH_EFFECT,
2929
INSPECT_EFFECT,
3030
UNOWNED,
31-
MAYBE_DIRTY
31+
MAYBE_DIRTY,
32+
BLOCK_EFFECT
3233
} from '../constants.js';
3334
import * as e from '../errors.js';
3435

@@ -136,14 +137,24 @@ export function set(source, value) {
136137
if (
137138
active_reaction !== null &&
138139
is_runes() &&
139-
(active_reaction.f & DERIVED) !== 0 &&
140+
(active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 &&
140141
// If the source was created locally within the current derived, then
141142
// we allow the mutation.
142143
(derived_sources === null || !derived_sources.includes(source))
143144
) {
144145
e.state_unsafe_mutation();
145146
}
146147

148+
return internal_set(source, value);
149+
}
150+
151+
/**
152+
* @template V
153+
* @param {Source<V>} source
154+
* @param {V} value
155+
* @returns {V}
156+
*/
157+
export function internal_set(source, value) {
147158
if (!source.equals(value)) {
148159
source.v = value;
149160
source.version = increment_version();
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
compileOptions: {
6+
dev: true
7+
},
8+
9+
test({ assert, target }) {
10+
const button = target.querySelector('button');
11+
12+
assert.throws(() => {
13+
button?.click();
14+
flushSync();
15+
}, /state_unsafe_mutation/);
16+
}
17+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script>
2+
let items = $state([]);
3+
</script>
4+
5+
<button onclick={() => items.push(3, 2, 1)}>Add</button>
6+
{#each items.sort() as item (item)}
7+
<p>{item}</p>
8+
{/each}
9+

0 commit comments

Comments
 (0)