Skip to content

Commit cc273f7

Browse files
authored
fix: prevent infinite loop when writing to store using shorthand (#10477)
Fixes #10472 This PR ensures we untrack parts of the compiled output to a store write, such as that this no longer brings up an infinite updates error
1 parent 87d4b12 commit cc273f7

File tree

4 files changed

+78
-2
lines changed

4 files changed

+78
-2
lines changed

.changeset/old-jokes-deliver.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: prevent infinite loop when writing to store using shorthand

packages/svelte/src/compiler/phases/3-transform/client/utils.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,15 +418,44 @@ export function serialize_set_binding(node, context, fallback, options) {
418418
}
419419
} else {
420420
if (is_store) {
421+
// If we are assigning to a store property, we need to ensure we don't
422+
// capture the read for the store as part of the member expression to
423+
// keep consistency with how store $ shorthand reads work in Svelte 4.
424+
/**
425+
*
426+
* @param {import("estree").Expression | import("estree").Pattern} node
427+
* @returns {import("estree").Expression}
428+
*/
429+
function visit_node(node) {
430+
if (node.type === 'MemberExpression') {
431+
return {
432+
...node,
433+
object: visit_node(/** @type {import("estree").Expression} */ (node.object)),
434+
property: /** @type {import("estree").Expression} */ (visit(node.property))
435+
};
436+
}
437+
if (node.type === 'Identifier') {
438+
const binding = state.scope.get(node.name);
439+
440+
if (binding !== null && binding.kind === 'store_sub') {
441+
return b.call(
442+
'$.untrack',
443+
b.thunk(/** @type {import('estree').Expression} */ (visit(node)))
444+
);
445+
}
446+
}
447+
return /** @type {import("estree").Expression} */ (visit(node));
448+
}
449+
421450
return b.call(
422451
'$.mutate_store',
423452
serialize_get_binding(b.id(left_name), state),
424453
b.assignment(
425454
node.operator,
426-
/** @type {import('estree').Pattern} */ (visit(node.left)),
455+
/** @type {import("estree").Pattern}} */ (visit_node(node.left)),
427456
value
428457
),
429-
b.call('$' + left_name)
458+
b.call('$.untrack', b.id('$' + left_name))
430459
);
431460
} else if (!state.analysis.runes) {
432461
if (binding.kind === 'prop') {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { flushSync } from '../../../../src/main/main-client';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
const btn = target.querySelector('button');
7+
8+
flushSync(() => {
9+
btn?.click();
10+
});
11+
12+
assert.htmlEqual(
13+
target.innerHTML,
14+
`<p>test_store:\n 4</p><p>counter:\n 4</p><button>+1</button>`
15+
);
16+
17+
flushSync(() => {
18+
btn?.click();
19+
});
20+
21+
assert.htmlEqual(
22+
target.innerHTML,
23+
`<p>test_store:\n 5</p><p>counter:\n 5</p><button>+1</button>`
24+
);
25+
}
26+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script>
2+
import { writable } from 'svelte/store'
3+
4+
let test_store = writable({id:0});
5+
let counter = $state(3);
6+
7+
$effect(() => {
8+
$test_store.id = counter
9+
});
10+
</script>
11+
12+
13+
<p>test_store: {$test_store.id}</p>
14+
<p>counter: {counter}</p>
15+
16+
<button onclick={() => counter++}>+1</button>

0 commit comments

Comments
 (0)