Skip to content

Commit

Permalink
fix: invalidate store when mutated inside each block
Browse files Browse the repository at this point in the history
fixes #10771
  • Loading branch information
dummdidumm committed Mar 13, 2024
1 parent 0c1026f commit 0758095
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/khaki-ligers-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

fix: invalidate store when mutated inside each block
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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);
}
};
};
Expand Down
11 changes: 11 additions & 0 deletions packages/svelte/src/internal/client/reactivity/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ok, test } from '../../test';

export default test({
skip_if_ssr: 'permanent',
html: `
<input type="checkbox">
<input type="checkbox">
<input type="checkbox">
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,
`
<input type="checkbox">
<input type="checkbox">
<input type="checkbox">
1
`
);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script>
import { derived, writable } from "svelte/store";
const checks = writable([false, false, false])
const countChecked = derived(checks, ($checks) => $checks.filter(Boolean).length)
</script>

{#each $checks as checked}
<input type="checkbox" bind:checked />
{/each}

{$countChecked}

0 comments on commit 0758095

Please sign in to comment.