Repository: sveltejs/svelte (JavaScript)
Date: 2020-09-13 05:21:55+00:00
Task type: Bug Fix

Issue:
Can't use destructuring assignment to get 2 stores from object, when variables declared with let
**Describe the bug**

When using object destructuring assignment to extract 2 stores out of an object, like this:
```javascript
let store1
let store2
({
	store1,
	store2,
} = context);
```

, then only whichever store is assigned to a variable _first_ (in this case `store1`) can be used. The other one is undefined when you (auto)subscribe to it (`$store2`).

**To Reproduce**

I have tried many variations to try to identify which factors are necessary and sufficient conditions for this strange behavior to occur. The 1 broken behavior and various variations that seem to work fine are summarized in this [REPL repro](https://svelte.dev/repl/ba980bfbd031448b8f49500f5900f305?version=3.25.0).

It appears that this strange behavior — `$store2` being undefined (see `Broken.svelte`) — only occurs iff all of these conditions are true:

<ol>
	<li>We use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment">destructuring assignment </a> instead of regular assignment (see <code>AssignWithoutDestructure1.svelte</code>)</li>
	<li>Reference <code>$store1</code> at all (see <code>OnlyReferenceStore2.svelte</code>)</li>
	<li>Use <code>let</code> instead of <code>const</code> (see <code>Const.svelte</code>)</li>
	<li>Destructure/assign <code>store1</code> first (see <code>AssignStore2First.svelte</code>)</li>
</ol>

Why exactly can I not use this combination of factors in my Svelte component? What is the rule that I am breaking, and where is it documented?

**Expected behavior**

I expect to be able to use `let` and destructuring assignment with store objects just the same as I can with any other type of value.

This works just fine, for example:
```javascript
let store1
let store2
({
	store1,
	store2,
} = {store1: 'value1', store2: 'value2'});

// store1 => "value1"
// store2 => "value2"
```
so I don't think there's anything wrong with my JavaScript. So what is so special about _stores_ as values that would make it not work? And why does `let` vs. `const` make a difference here? Is the Svelte compiler doing something special with `let`s (even though I'm not using `$:` here) that is causing this strange behavior?

**Severity**

It seems medium severe to me. I can _probably_ work around it (I will attempt that next), but it certainly diminishes my confidence in Svelte when:
1. a Svelte store mysteriously reports being `undefined` for no apparent reason
2. something that seems like standard JavaScript just doesn't work as expected and I have to come up with an arbitrarily different way to write it

Maybe I'm just doing something wrong, but I couldn't figure it out (after hours of turning this into a simple as possible repro), so I reported this as a bug until proven otherwise... :)



Issue discussion:
In the first broken example, the compiler is outputting

```js
	let store1;
	$$subscribe_store1();
	let store2;
	$$subscribe_store2();
	$$subscribe_store1({ store1, store2 } = context);
```

which is incorrect - as opposed to the example without destructuring, where it is outputting

```js
	let store1;
	$$subscribe_store1();
	let store2;
	$$subscribe_store2();
	$$subscribe_store1(store1 = context.store1);
	$$subscribe_store2(store2 = context.store2);
```

We need to make sure _both_ subscription functions are called after the destructuring assignment happens. So we can continue to use the destructuring in the output, probably what makes them most sense is to make two separate subscription calls afterwards.

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 * Fix specificity of certain styles involving a child selector ([#4795](https://github.com/sveltejs/svelte/issues/4795))
 * Fix transitions that are parameterised with stores ([#5244](https://github.com/sveltejs/svelte/issues/5244))
 * Fix scoping of styles involving child selector and `*` ([#5370](https://github.com/sveltejs/svelte/issues/5370))
+* Fix destructuring which reassigns stores ([#5388](https://github.com/sveltejs/svelte/issues/5388))
 
 ## 3.25.0
 
diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts
--- a/src/compiler/compile/Component.ts
+++ b/src/compiler/compile/Component.ts
@@ -947,12 +947,6 @@ export default class Component {
 							const variable = component.var_lookup.get(name);
 
 							if (variable.export_name && variable.writable) {
-								const insert = variable.subscribable
-									? get_insert(variable)
-									: null;
-
-								parent[key].splice(index + 1, 0, insert);
-
 								declarator.id = {
 									type: 'ObjectPattern',
 									properties: [{
@@ -973,7 +967,9 @@ export default class Component {
 								};
 
 								declarator.init = x`$$props`;
-							} else if (variable.subscribable) {
+							}
+
+							if (variable.subscribable && declarator.init) {
 								const insert = get_insert(variable);
 								parent[key].splice(index + 1, 0, ...insert);
 							}
diff --git a/src/compiler/compile/render_dom/Renderer.ts b/src/compiler/compile/render_dom/Renderer.ts
--- a/src/compiler/compile/render_dom/Renderer.ts
+++ b/src/compiler/compile/render_dom/Renderer.ts
@@ -166,12 +166,14 @@ export default class Renderer {
 		return member;
 	}
 
-	invalidate(name: string, value?) {
+	invalidate(name: string, value?, main_execution_context: boolean = false) {
 		const variable = this.component.var_lookup.get(name);
 		const member = this.context_lookup.get(name);
 
 		if (variable && (variable.subscribable && (variable.reassigned || variable.export_name))) {
-			return x`${`$$subscribe_${name}`}($$invalidate(${member.index}, ${value || name}))`;
+			return main_execution_context
+			  ? x`${`$$subscribe_${name}`}(${value || name})`
+			  : x`${`$$subscribe_${name}`}($$invalidate(${member.index}, ${value || name}))`;
 		}
 
 		if (name[0] === '$' && name[1] !== '$') {
diff --git a/src/compiler/compile/render_dom/invalidate.ts b/src/compiler/compile/render_dom/invalidate.ts
--- a/src/compiler/compile/render_dom/invalidate.ts
+++ b/src/compiler/compile/render_dom/invalidate.ts
@@ -31,10 +31,9 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names:
 
 	function get_invalidated(variable: Var, node?: Expression) {
 		if (main_execution_context && !variable.subscribable && variable.name[0] !== '$') {
-			return node || x`${variable.name}`;
+			return node;
 		}
-
-		return renderer.invalidate(variable.name);
+		return renderer.invalidate(variable.name, undefined, main_execution_context);
 	}
 
 	if (head) {
@@ -44,12 +43,15 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names:
 			return get_invalidated(head, node);
 		} else {
 			const is_store_value = head.name[0] === '$' && head.name[1] !== '$';
-			const extra_args = tail.map(variable => get_invalidated(variable));
+			const extra_args = tail.map(variable => get_invalidated(variable)).filter(Boolean);
 
 			const pass_value = (
-				extra_args.length > 0 ||
-				(node.type === 'AssignmentExpression' && node.left.type !== 'Identifier') ||
-				(node.type === 'UpdateExpression' && (!node.prefix || node.argument.type !== 'Identifier'))
+				!main_execution_context && 
+				(
+					extra_args.length > 0 ||
+					(node.type === 'AssignmentExpression' && node.left.type !== 'Identifier') ||
+					(node.type === 'UpdateExpression' && (!node.prefix || node.argument.type !== 'Identifier'))
+				)
 			);
 
 			if (pass_value) {
@@ -63,7 +65,9 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names:
 				? x`@set_store_value(${head.name.slice(1)}, ${node}, ${extra_args})`
 				: !main_execution_context
 					? x`$$invalidate(${renderer.context_lookup.get(head.name).index}, ${node}, ${extra_args})`
-					: node;
+					: extra_args.length
+						? [node, ...extra_args]
+						: node;
 
 			if (head.subscribable && head.reassigned) {
 				const subscribe = `$$subscribe_${head.name}`;


Test patch:
diff --git a/test/runtime/samples/reactive-assignment-in-complex-declaration-with-store/_config.js b/test/runtime/samples/reactive-assignment-in-complex-declaration-with-store/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/reactive-assignment-in-complex-declaration-with-store/_config.js
@@ -0,0 +1,3 @@
+export default {
+	html: `<h1>2 2 xxx 5 6</h1>`
+};
\ No newline at end of file
diff --git a/test/runtime/samples/reactive-assignment-in-complex-declaration-with-store/main.svelte b/test/runtime/samples/reactive-assignment-in-complex-declaration-with-store/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/reactive-assignment-in-complex-declaration-with-store/main.svelte
@@ -0,0 +1,16 @@
+<script>
+	import { writable } from 'svelte/store';
+
+	let eid = writable(1);
+	let foo;
+	let u;
+	let v;
+	let w;
+	[u, v, w] = [
+		{id: eid = writable(foo = 2), name: 'xxx'},
+		5,
+		writable(6)
+	];
+</script>
+
+<h1>{foo} {$eid} {u.name} {v} {$w}</h1>
diff --git a/test/runtime/samples/reactive-assignment-in-declaration/main.svelte b/test/runtime/samples/reactive-assignment-in-declaration/main.svelte
--- a/test/runtime/samples/reactive-assignment-in-declaration/main.svelte
+++ b/test/runtime/samples/reactive-assignment-in-declaration/main.svelte
@@ -1,6 +1,9 @@
 <script>
 	let foo;
 	let bar = (foo = 1);
+	function a() {
+		bar = (foo = 1);
+	}
 </script>
 
 <h1>{foo} {bar}</h1>
diff --git a/test/runtime/samples/reactive-assignment-in-for-loop-head/main.svelte b/test/runtime/samples/reactive-assignment-in-for-loop-head/main.svelte
--- a/test/runtime/samples/reactive-assignment-in-for-loop-head/main.svelte
+++ b/test/runtime/samples/reactive-assignment-in-for-loop-head/main.svelte
@@ -1,9 +1,14 @@
 <script>
-  let foo1;
-  let foo2;
-  for (let bar = (foo1 = 0); bar < 5; bar += 1) {
-    foo2 = foo1;
-  }
+	let foo1;
+	let foo2;
+	for (let bar = (foo1 = 0); bar < 5; bar += 1) {
+		foo2 = foo1;
+	}
+	function a() {
+		for (let bar = (foo1 = 0); bar < 5; bar += 1) {
+			foo2 = foo1;
+		}
+	}
 </script>
 
 <h1>{foo1} {foo2}</h1>
diff --git a/test/runtime/samples/store-resubscribe-c/_config.js b/test/runtime/samples/store-resubscribe-c/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/store-resubscribe-c/_config.js
@@ -0,0 +1,3 @@
+export default {
+	html: `31 42`
+};
diff --git a/test/runtime/samples/store-resubscribe-c/main.svelte b/test/runtime/samples/store-resubscribe-c/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/store-resubscribe-c/main.svelte
@@ -0,0 +1,16 @@
+<script>
+	import { writable } from 'svelte/store';
+	const context = {
+		store1: writable(31),
+		store2: writable(42)
+	};
+	let store1;
+	let store2;
+	({
+		store1,
+		store2
+	} = context);
+</script>
+
+{$store1}
+{$store2}