Skip to content

Commit fbbd89a

Browse files
authored
fix: invalidate store when mutated inside each block (#10785)
* fix: invalidate store when mutated inside each block fixes #10771 * Update packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
1 parent bd687c6 commit fbbd89a

File tree

5 files changed

+82
-4
lines changed

5 files changed

+82
-4
lines changed

.changeset/khaki-ligers-sing.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: invalidate store when mutated inside each block

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

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2344,8 +2344,20 @@ export const template_visitors = {
23442344
each_type |= EACH_IS_STRICT_EQUALS;
23452345
}
23462346

2347-
// Find the parent each blocks which contain the arrays to invalidate
2348-
// TODO decide how much of this we want to keep for runes mode. For now we're bailing out below
2347+
// If the array is a store expression, we need to invalidate it when the array is changed.
2348+
// This doesn't catch all cases, but all the ones that Svelte 4 catches, too.
2349+
let store_to_invalidate = '';
2350+
if (node.expression.type === 'Identifier' || node.expression.type === 'MemberExpression') {
2351+
const id = object(node.expression);
2352+
if (id) {
2353+
const binding = context.state.scope.get(id.name);
2354+
if (binding?.kind === 'store_sub') {
2355+
store_to_invalidate = id.name;
2356+
}
2357+
}
2358+
}
2359+
2360+
// Legacy mode: find the parent each blocks which contain the arrays to invalidate
23492361
const indirect_dependencies = collect_parent_each_blocks(context).flatMap((block) => {
23502362
const array = /** @type {import('estree').Expression} */ (context.visit(block.expression));
23512363
const transitive_dependencies = serialize_transitive_dependencies(
@@ -2382,15 +2394,24 @@ export const template_visitors = {
23822394
'$.invalidate_inner_signals',
23832395
b.thunk(b.sequence(indirect_dependencies))
23842396
);
2397+
const invalidate_store = store_to_invalidate
2398+
? b.call('$.invalidate_store', b.id('$$subscriptions'), b.literal(store_to_invalidate))
2399+
: undefined;
2400+
2401+
const sequence = [];
2402+
if (!context.state.analysis.runes) sequence.push(invalidate);
2403+
if (invalidate_store) sequence.push(invalidate_store);
23852404

23862405
if (left === assignment.left) {
23872406
const assign = b.assignment('=', expression_for_id, value);
2388-
return context.state.analysis.runes ? assign : b.sequence([assign, invalidate]);
2407+
sequence.unshift(assign);
2408+
return b.sequence(sequence);
23892409
} else {
23902410
const original_left = /** @type {import('estree').MemberExpression} */ (assignment.left);
23912411
const left = context.visit(original_left);
23922412
const assign = b.assignment(assignment.operator, left, value);
2393-
return context.state.analysis.runes ? assign : b.sequence([assign, invalidate]);
2413+
sequence.unshift(assign);
2414+
return b.sequence(sequence);
23942415
}
23952416
};
23962417
};

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,17 @@ export function store_set(store, value) {
8989
return value;
9090
}
9191

92+
/**
93+
* @param {import('#client').StoreReferencesContainer} stores
94+
* @param {string} store_name
95+
*/
96+
export function invalidate_store(stores, store_name) {
97+
const store = stores[store_name];
98+
if (store.store) {
99+
store_set(store.store, store.value.v);
100+
}
101+
}
102+
92103
/**
93104
* Unsubscribes from all auto-subscribed stores on destroy
94105
* @param {import('#client').StoreReferencesContainer} stores
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ok, test } from '../../test';
2+
3+
export default test({
4+
skip_if_ssr: 'permanent',
5+
html: `
6+
<input type="checkbox">
7+
<input type="checkbox">
8+
<input type="checkbox">
9+
0
10+
`,
11+
12+
async test({ assert, target, window }) {
13+
const input = target.querySelector('input');
14+
ok(input);
15+
16+
input.checked = true;
17+
await input.dispatchEvent(new window.Event('change', { bubbles: true }));
18+
19+
assert.htmlEqual(
20+
target.innerHTML,
21+
`
22+
<input type="checkbox">
23+
<input type="checkbox">
24+
<input type="checkbox">
25+
1
26+
`
27+
);
28+
}
29+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script>
2+
import { derived, writable } from "svelte/store";
3+
4+
const checks = writable([false, false, false])
5+
const countChecked = derived(checks, ($checks) => $checks.filter(Boolean).length)
6+
</script>
7+
8+
{#each $checks as checked}
9+
<input type="checkbox" bind:checked />
10+
{/each}
11+
12+
{$countChecked}

0 commit comments

Comments
 (0)