Repository: sveltejs/svelte (JavaScript) — Bug Fix

Issue:
Store does not update component in certain combination with bind + #if + store
**Describe the bug** / **To Reproduce**
We encountered a weird edge case in our app where where one of the stores didn't trigger any updates in a child component sometimes.
There are multiple steps necessary for this bug to happen, so I'll try to explain it in a step-by-step way, otherwise it gets unreadable.

The step-by-step is tied to this REPL:
https://svelte.dev/repl/ae73276603ff41048ac558daae06bf4a?version=3.29.0

In the basic outline `App` has two childs:
  - `Bind` which will set `displayPanel` to `true` when `$x != undefined` and show `Subscr`
  - `Subscr` which subscribes to `$x` in code within a reactive block `$:`
  - And we have a global store `x` initialized to `undefined`

When clicking on start:
1. store `x` is set to "1"
2. `Bind` will notice this and set `displayPanel` to true
3. `App` will notice this and render the `{#if}` Block
4. `Subscr` will render with the store value correctly set and show `E: 1`
5. store `x` is set to `undefinded`
6. `Subscr` will *not* notice this update, even though `$x` is in the recative block. In fact the reactive block does not get executed at all (check the console output).
7. Only setting `$x` to something that is not `undefined` and then setting it back to `undefined` the `Subscr` component will notice each update again.

You can see in the console log that the first set to undefined does not get noticed by the `Subscr` component.
```
Set to e
update 1
Set to undef
Set to e
update 2
Set to undef
update undefined
```

From my debugging it *seems* like `Subscr` has a stale value of `x` when it gets created but `$x` has the new value. When then setting `x` to `undefined` the update will not execute since the old value already is `undefined`. But I might be wrong here.

Also when using the $x variable in the html this problem does not occour and the store gets updated correctly. (I've added a comment in the `Subscr` to mark this)

**Expected behavior**
The reactive statement should correctly be executed with the store change.

**Information about your Svelte project:**
- Your browser and the version: Firefox 81.0.2 (64-bit)
- Your operating system: Windows 10 Education
- Svelte version 3.29.0
- Using rollup (but should not relevant for this case)

**Severity**
Low to Medium:
The bug itself can be easily work around by adding the store somewhere in the html part, but finding and debugging the cause is a nightmare.


Discussion:

Resolving patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 
+* Fix setting reactive dependencies which don't appear in the template to `undefined` ([#5538](https://github.com/sveltejs/svelte/issues/5538))
 * Fix ordering of elements when using `{#if}` inside `{#key}` ([#5680](https://github.com/sveltejs/svelte/issues/5680))
 * Add `hasContext` lifecycle function ([#5690](https://github.com/sveltejs/svelte/pull/5690))
 * Fix missing `walk` types in `svelte/compiler` ([#5696](https://github.com/sveltejs/svelte/pull/5696))
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
@@ -111,8 +111,9 @@ export default class Renderer {
 
 				// these determine whether variable is included in initial context
 				// array, so must have the highest priority
-				if (variable.export_name) member.priority += 16;
-				if (variable.referenced) member.priority += 32;
+				if (variable.is_reactive_dependency && (variable.mutated || variable.reassigned)) member.priority += 16;
+				if (variable.export_name) member.priority += 32;
+				if (variable.referenced) member.priority += 64;
 			} else if (member.is_non_contextual) {
 				// determine whether variable is included in initial context
 				// array, so must have the highest priority
@@ -131,7 +132,7 @@ export default class Renderer {
 		while (i--) {
 			const member = this.context[i];
 			if (member.variable) {
-				if (member.variable.referenced || member.variable.export_name) break;
+				if (member.variable.referenced || member.variable.export_name || (member.variable.is_reactive_dependency && (member.variable.mutated || member.variable.reassigned))) break;
 			} else if (member.is_non_contextual) {
 				break;
 			}


Test patch:
diff --git a/test/js/samples/instrumentation-script-main-block/expected.js b/test/js/samples/instrumentation-script-main-block/expected.js
--- a/test/js/samples/instrumentation-script-main-block/expected.js
+++ b/test/js/samples/instrumentation-script-main-block/expected.js
@@ -62,7 +62,7 @@ function instance($$self, $$props, $$invalidate) {
 		}
 	};
 
-	return [x];
+	return [x, y];
 }
 
 class Component extends SvelteComponent {
diff --git a/test/js/samples/reactive-values-non-topologically-ordered/expected.js b/test/js/samples/reactive-values-non-topologically-ordered/expected.js
--- a/test/js/samples/reactive-values-non-topologically-ordered/expected.js
+++ b/test/js/samples/reactive-values-non-topologically-ordered/expected.js
@@ -12,15 +12,15 @@ function instance($$self, $$props, $$invalidate) {
 
 	$$self.$$.update = () => {
 		if ($$self.$$.dirty & /*x*/ 1) {
-			$: $$invalidate(2, b = x);
+			$: $$invalidate(1, b = x);
 		}
 
-		if ($$self.$$.dirty & /*b*/ 4) {
+		if ($$self.$$.dirty & /*b*/ 2) {
 			$: a = b;
 		}
 	};
 
-	return [x];
+	return [x, b];
 }
 
 class Component extends SvelteComponent {
diff --git a/test/js/samples/unreferenced-state-not-invalidated/expected.js b/test/js/samples/unreferenced-state-not-invalidated/expected.js
--- a/test/js/samples/unreferenced-state-not-invalidated/expected.js
+++ b/test/js/samples/unreferenced-state-not-invalidated/expected.js
@@ -64,7 +64,7 @@ function instance($$self, $$props, $$invalidate) {
 	};
 
 	$: x = a * 2;
-	return [y];
+	return [y, b];
 }
 
 class Component extends SvelteComponent {
diff --git a/test/runtime/samples/reactive-value-dependency-not-referenced/_config.js b/test/runtime/samples/reactive-value-dependency-not-referenced/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/reactive-value-dependency-not-referenced/_config.js
@@ -0,0 +1,26 @@
+export default {
+	html: `
+		<p>42</p>
+		<p>42</p>
+	`,
+
+	async test({ assert, component, target }) {
+		await component.updateStore(undefined);
+		assert.htmlEqual(target.innerHTML, '<p>undefined</p><p>42</p>');
+
+		await component.updateStore(33);
+		assert.htmlEqual(target.innerHTML, '<p>33</p><p>42</p>');
+
+		await component.updateStore(undefined);
+		assert.htmlEqual(target.innerHTML, '<p>undefined</p><p>42</p>');
+
+		await component.updateVar(undefined);
+		assert.htmlEqual(target.innerHTML, '<p>undefined</p><p>undefined</p>');
+
+		await component.updateVar(33);
+		assert.htmlEqual(target.innerHTML, '<p>undefined</p><p>33</p>');
+
+		await component.updateVar(undefined);
+		assert.htmlEqual(target.innerHTML, '<p>undefined</p><p>undefined</p>');
+	}
+};
diff --git a/test/runtime/samples/reactive-value-dependency-not-referenced/main.svelte b/test/runtime/samples/reactive-value-dependency-not-referenced/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/reactive-value-dependency-not-referenced/main.svelte
@@ -0,0 +1,19 @@
+<script>
+	import { writable } from 'svelte/store';
+	let store = writable(42);
+	let variable = 42;
+	let value;
+	let value2;
+	$: value = $store;
+	$: value2 = variable;
+
+	export function updateStore(value) {
+		store.set(value);
+	}
+	export function updateVar(value) {
+		variable = value;
+	}
+</script>
+
+<p>{ value }</p>
+<p>{ value2 }</p>
\ No newline at end of file