diff --git a/.changeset/khaki-ligers-sing.md b/.changeset/khaki-ligers-sing.md
new file mode 100644
index 000000000000..d2a1fa9b7e5d
--- /dev/null
+++ b/.changeset/khaki-ligers-sing.md
@@ -0,0 +1,5 @@
+---
+"svelte": patch
+---
+
+fix: invalidate store when mutated inside each block
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
index 86246742a4ce..84458993905e 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
@@ -2344,8 +2344,19 @@ export const template_visitors = {
each_type |= EACH_IS_STRICT_EQUALS;
}
- // Find the parent each blocks which contain the arrays to invalidate
- // TODO decide how much of this we want to keep for runes mode. For now we're bailing out below
+ // If the array is a store expression, we need to invalidate it when the array is changed
+ let store_to_invalidate = '';
+ if (node.expression.type === 'Identifier' || node.expression.type === 'MemberExpression') {
+ const id = object(node.expression);
+ if (id) {
+ const binding = context.state.scope.get(id.name);
+ if (binding?.kind === 'store_sub') {
+ store_to_invalidate = id.name;
+ }
+ }
+ }
+
+ // Legacy mode: find the parent each blocks which contain the arrays to invalidate
const indirect_dependencies = collect_parent_each_blocks(context).flatMap((block) => {
const array = /** @type {import('estree').Expression} */ (context.visit(block.expression));
const transitive_dependencies = serialize_transitive_dependencies(
@@ -2382,15 +2393,24 @@ export const template_visitors = {
'$.invalidate_inner_signals',
b.thunk(b.sequence(indirect_dependencies))
);
+ const invalidate_store = store_to_invalidate
+ ? b.call('$.invalidate_store', b.id('$$subscriptions'), b.literal(store_to_invalidate))
+ : undefined;
+
+ const sequence = [];
+ if (!context.state.analysis.runes) sequence.push(invalidate);
+ if (invalidate_store) sequence.push(invalidate_store);
if (left === assignment.left) {
const assign = b.assignment('=', expression_for_id, value);
- return context.state.analysis.runes ? assign : b.sequence([assign, invalidate]);
+ sequence.unshift(assign);
+ return b.sequence(sequence);
} else {
const original_left = /** @type {import('estree').MemberExpression} */ (assignment.left);
const left = context.visit(original_left);
const assign = b.assignment(assignment.operator, left, value);
- return context.state.analysis.runes ? assign : b.sequence([assign, invalidate]);
+ sequence.unshift(assign);
+ return b.sequence(sequence);
}
};
};
diff --git a/packages/svelte/src/internal/client/reactivity/store.js b/packages/svelte/src/internal/client/reactivity/store.js
index 7ea57428da16..0529f099a431 100644
--- a/packages/svelte/src/internal/client/reactivity/store.js
+++ b/packages/svelte/src/internal/client/reactivity/store.js
@@ -95,6 +95,17 @@ export function store_set(store, value) {
return value;
}
+/**
+ * @param {import('#client').StoreReferencesContainer} stores
+ * @param {string} store_name
+ */
+export function invalidate_store(stores, store_name) {
+ const store = stores[store_name];
+ if (store.store) {
+ store_set(store.store, store.value.v);
+ }
+}
+
/**
* Unsubscribes from all auto-subscribed stores on destroy
* @param {import('#client').StoreReferencesContainer} stores
diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-store-each/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-store-each/_config.js
new file mode 100644
index 000000000000..cd0690f2dc2e
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/binding-store-each/_config.js
@@ -0,0 +1,29 @@
+import { ok, test } from '../../test';
+
+export default test({
+ skip_if_ssr: 'permanent',
+ html: `
+
+
+
+ 0
+ `,
+
+ async test({ assert, target, window }) {
+ const input = target.querySelector('input');
+ ok(input);
+
+ input.checked = true;
+ await input.dispatchEvent(new window.Event('change', { bubbles: true }));
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+ 1
+ `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-store-each/main.svelte b/packages/svelte/tests/runtime-legacy/samples/binding-store-each/main.svelte
new file mode 100644
index 000000000000..e15cc3f6fff0
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/binding-store-each/main.svelte
@@ -0,0 +1,12 @@
+
+
+{#each $checks as checked}
+
+{/each}
+
+{$countChecked}