diff --git a/.changeset/brown-plums-prove.md b/.changeset/brown-plums-prove.md new file mode 100644 index 000000000000..7fbac0bb0e41 --- /dev/null +++ b/.changeset/brown-plums-prove.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: only make variables reactive if they are read (and reassigned) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 1f636c32df6d..b6f5acda7591 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -389,6 +389,10 @@ export function analyze_component(root, source, options) { }); const binding = instance.scope.declare(b.id(name), 'store_sub', 'synthetic'); + const store_binding = instance.scope.get(name.slice(1)); + if (store_binding) { + store_binding.has_store_sub = true; + } binding.references = references; instance.scope.references.set(name, references); module.scope.references.delete(name); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 421118cf680b..92c3ac21f9c8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -23,7 +23,9 @@ import { get_value } from './visitors/shared/declarations.js'; export function is_state_source(binding, analysis) { return ( (binding.kind === 'state' || binding.kind === 'raw_state') && - (!analysis.immutable || binding.reassigned || analysis.accessors) + (!analysis.immutable || + (binding.reassigned && (binding.read || binding.has_store_sub)) || + analysis.accessors) ); } diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index b6063c32343f..2fd1a344a4f2 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -59,7 +59,8 @@ export class Binding { mutated = false; reassigned = false; - + read = false; + has_store_sub = false; /** * * @param {Scope} scope @@ -310,7 +311,7 @@ export class ScopeRoot { * @param {Scope | null} parent */ export function create_scopes(ast, root, allow_reactive_declarations, parent) { - /** @typedef {{ scope: Scope }} State */ + /** @typedef {{ scope: Scope, reading: boolean }} State */ /** * A map of node->associated scope. A node appearing in this map does not necessarily mean that it created a scope @@ -321,14 +322,16 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { scopes.set(ast, scope); /** @type {State} */ - const state = { scope }; + const state = { scope, reading: true }; - /** @type {[Scope, { node: Identifier; path: AST.SvelteNode[] }][]} */ + /** @type {[Scope, { node: Identifier; path: AST.SvelteNode[] }, boolean][]} */ const references = []; /** @type {[Scope, Pattern | MemberExpression][]} */ const updates = []; + /** @type {[Scope, Pattern | MemberExpression][]} */ + const read_updates = []; /** * An array of reactive declarations, i.e. the `a` in `$: a = b * 2` * @type {Identifier[]} @@ -354,7 +357,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { const scope = state.scope.child(true); scopes.set(node, scope); - next({ scope }); + next({ scope, reading: state.reading }); }; /** @@ -363,7 +366,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { const SvelteFragment = (node, { state, next }) => { const scope = state.scope.child(); scopes.set(node, scope); - next({ scope }); + next({ scope, reading: state.reading }); }; /** @@ -380,7 +383,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { const default_state = determine_slot(node) ? context.state - : { scope: node.metadata.scopes.default }; + : { scope: node.metadata.scopes.default, reading: true }; for (const attribute of node.attributes) { if (attribute.type === 'LetDirective') { @@ -399,7 +402,8 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { node.metadata.scopes[slot_name] = context.state.scope.child(); state = { - scope: node.metadata.scopes[slot_name] + scope: node.metadata.scopes[slot_name], + reading: true }; } @@ -430,10 +434,10 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { // TODO -> once migration script is gone we can remove this check !parent.type.startsWith('TS') ) { - references.push([state.scope, { node, path: path.slice() }]); + references.push([state.scope, { node, path: path.slice() }, state.reading]); } }, - LabeledStatement(node, { path, next }) { + LabeledStatement(node, { state, path, next }) { if (path.length > 1 || !allow_reactive_declarations) return next(); if (node.label.name !== '$') return next(); @@ -452,7 +456,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { } } - next({ scope }); + next({ scope, reading: state.reading }); }, SvelteFragment, @@ -495,13 +499,22 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { SvelteComponent: Component, // updates - AssignmentExpression(node, { state, next }) { + AssignmentExpression(node, { state, next, visit }) { updates.push([state.scope, node.left]); - next(); + let reading = state.reading; + if (node.operator !== '=') { + reading = true; + } else { + reading = false; + } + visit(node.left, { ...state, reading }); + visit(node.right, state); }, UpdateExpression(node, { state, next }) { updates.push([state.scope, /** @type {Identifier | MemberExpression} */ (node.argument)]); + //@ts-ignore + read_updates.push(updates.at(-1)); next(); }, @@ -518,7 +531,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { if (node.id) scope.declare(node.id, 'normal', 'function'); add_params(scope, node.params); - next({ scope }); + next({ scope, reading: state.reading }); }, FunctionDeclaration(node, { state, next }) { @@ -528,7 +541,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { scopes.set(node, scope); add_params(scope, node.params); - next({ scope }); + next({ scope, reading: state.reading }); }, ArrowFunctionExpression(node, { state, next }) { @@ -536,7 +549,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { scopes.set(node, scope); add_params(scope, node.params); - next({ scope }); + next({ scope, reading: state.reading }); }, ForStatement: create_block_scope, @@ -593,7 +606,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { scope.declare(id, 'normal', 'let'); } - next({ scope }); + next({ scope, reading: state.reading }); } else { next(); } @@ -631,7 +644,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { } // Visit to pick up references from default initializers - visit(node.context, { scope }); + visit(node.context, { scope, reading: state.reading }); } if (node.index) { @@ -640,13 +653,13 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { (node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index); scope.declare(b.id(node.index), is_keyed ? 'template' : 'normal', 'const', node); } - if (node.key) visit(node.key, { scope }); + if (node.key) visit(node.key, { scope, reading: state.reading }); // children for (const child of node.body.nodes) { - visit(child, { scope }); + visit(child, { scope, reading: state.reading }); } - if (node.fallback) visit(node.fallback, { scope }); + if (node.fallback) visit(node.fallback, { scope, reading: state.reading }); node.metadata = { expression: create_expression_metadata(), @@ -671,7 +684,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { const then_scope = /** @type {Scope} */ (scopes.get(node.then)); const value_scope = context.state.scope.child(); scopes.set(node.value, value_scope); - context.visit(node.value, { scope: value_scope }); + context.visit(node.value, { scope: value_scope, reading: state.reading }); for (const id of extract_identifiers(node.value)) { then_scope.declare(id, 'template', 'const'); value_scope.declare(id, 'normal', 'const'); @@ -685,7 +698,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { const catch_scope = /** @type {Scope} */ (scopes.get(node.catch)); const error_scope = context.state.scope.child(); scopes.set(node.error, error_scope); - context.visit(node.error, { scope: error_scope }); + context.visit(node.error, { scope: error_scope, reading: state.reading }); for (const id of extract_identifiers(node.error)) { catch_scope.declare(id, 'template', 'const'); error_scope.declare(id, 'normal', 'const'); @@ -709,13 +722,13 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { } } - context.next({ scope: child_scope }); + context.next({ scope: child_scope, reading: state.reading }); }, Fragment: (node, context) => { const scope = context.state.scope.child(node.metadata.transparent); scopes.set(node, scope); - context.next({ scope }); + context.next({ scope, reading: state.reading }); }, BindDirective(node, context) { @@ -753,8 +766,12 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { // we do this after the fact, so that we don't need to worry // about encountering references before their declarations - for (const [scope, { node, path }] of references) { + for (const [scope, { node, path }, reading] of references) { scope.reference(node, path); + let binding = scope.get(node.name); + if (binding && binding.node !== node && reading) { + binding.read = true; + } } for (const [scope, node] of updates) { diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js index 9651713c52f5..d6a5f1e696bf 100644 --- a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js @@ -13,4 +13,5 @@ export function update(array) { ); [c, d] = array; + console.log({ a: $.get(a), b: $.get(b) }); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/server/index.svelte.js index 62b655b266f9..f97a58de750e 100644 --- a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/server/index.svelte.js @@ -9,4 +9,5 @@ let d = 4; export function update(array) { [a, b] = array; [c, d] = array; + console.log({ a, b }); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/index.svelte.js index 558cc66cc249..056ec72dffc8 100644 --- a/packages/svelte/tests/snapshot/samples/destructured-assignments/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/index.svelte.js @@ -6,4 +6,5 @@ let d = 4; export function update(array) { [a, b] = array; [c, d] = array; + console.log({ a, b }); }