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}