Repository: sveltejs/svelte (JavaScript)
Date: 2020-05-19 01:01:06+00:00
Task type: Bug Fix

Issue:
use-directives are called multiple times when elements inside a keyed block are reordered
**Description of the bug**
- When attaching "use" directives inside a keyed block, they are called again (without being destroyed) if the elements of the block are reordered. 

**To Reproduce**
- This REPL reproduces the issue: https://svelte.dev/repl/4b0807cd0596423b94e31a24bde9497a?version=3.20.1

**Expected behavior**
- Directives should be called only once when the component is mounted. 
- Alternatively, if the element is remounted, "destroy" should be called beforehand (e.g. to free previously created resources). 

**Information about the Svelte project:**
- Svelte version: 3.20.1 (bundled with Rollup)
- Browser: (latest) Chrome on MacOS

**Severity**
- Moderate to serious, since the application we are building is relying heavily on use-directives to leverage modularity. We noticed the issue when event listeners were mounted multiple times (leading to a memory leak as well as unexpected behavior and inconsistent internal state). 

**Additional context**
- The issue only happens with keyed each blocks. 

**Previous analysis**
- Here is where things break:

```javascript
/* src/ListItem.svelte generated by Svelte v3.20.1 */
    function create_fragment(ctx) {
    	let div;
    	let t;
    	let directive_action;
    	let dispose;

    	return {
    		c() {
    			div = element("div");
    			t = text(/*id*/ ctx[0]);
    		},
    		m(target, anchor, remount) {
    			insert(target, div, anchor);
    			append(div, t);
    			if (remount) dispose(); // <-- remount is undefined
    			dispose = action_destroyer(directive_action = directive.call(null, div, { id: /*id*/ ctx[0] }));
    		},
```

It seems that remounting _was_ considered, but the information (that the current operation is indeed a remount) gets lost due to an additional block in between. It's a bit hard to describe verbally. So here are the relevant parts of a debugger session (from the same app as in the example REPL):

![image](https://user-images.githubusercontent.com/40512168/79732420-3a664c00-8326-11ea-960b-7834c5a0bd97.png)


Issue discussion:
This came up before in #2735, which was recently closed by its author who said it was now fixed - but perhaps it wasn't completely? I haven't looked closely at either repro or looked again at the PR that was said to resolve the other issue.
Thanks. I did not see that one before (I had only searched for open issues...). This also explains where the (partial) solution came from.

Just had a look at the issue as well as the previous example.  The previous issue actually seems to be fixed because in that example, the keyed each block directly contains a div with the actions attached to it. 

Works:
```svelte
{#each ids as id (id)}
    <div use:directive={{id}}>{id}</div>
{/each}
```

Does not work:
```svelte
{#each ids as id (id)}
   <ListItem {id}/>
{/each}
```

I am not yet familiar with the Svelte compiler, but I believe it might be sufficient to change 3-4 areas:

1. Add remount as parameter to the mount call in additional circumstances (e.g. then this.type === 'each')
https://github.com/sveltejs/svelte/blob/fe003b57cb3076c1fc7d94b45a79ff397733186a/src/compiler/compile/render_dom/Block.ts#L298

2. Change the the signature of ```mount_component ``` by adding ```remount``` and then pass the ```remount``` attribute down the chain to the next component. https://github.com/sveltejs/svelte/blob/ff5f25249e39985b6ca88c6815d123f26e40402a/src/runtime/internal/Component.ts#L55 
Either with a default of false, or with an additional change a few lines down when a component is initially mounted. 
https://github.com/sveltejs/svelte/blob/ff5f25249e39985b6ca88c6815d123f26e40402a/src/runtime/internal/Component.ts#L159

3. Add remount here:
https://github.com/sveltejs/svelte/blob/fe003b57cb3076c1fc7d94b45a79ff397733186a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts#L512

4. Not sure about this one:
https://github.com/sveltejs/svelte/blob/fe003b57cb3076c1fc7d94b45a79ff397733186a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts#L438

Today evening I already attempted to change 1-3 and it worked in my small example (destroy was called for each action), but a whole lot of tests did not pass any more after the change and I have not yet figured out why. @tanhauhau : Looks like you are (much) more familiar with the compiler internals. Maybe you can have a look if my hunch is going in the right direction or not. 
I had a second attempt at this today morning and got it working: All (non-JS) tests were passing and the issue was fixed in a test environment. So I continued to also update the JS tests (which compare the source code 1:1). Now all tests are passing again. 

Also tried this on a rather large & complex app (70K+ LOC) and it worked exactly as expected (remark: I don't use SSR, but I assume the tests should cover this). 

I guess it would be prudent to also write a test for this specific case (which I have not yet done), but before I take a shot at that and create a PR, it would be great if someone more experienced with the compiler internals could have a quick look at my changes: 
https://github.com/sveltejs/svelte/compare/master...christianheine:master
After additional testing, I realized that there were still cases when ```remount``` was not passed along the whole (re)mounting chain. This was a bit trickier to find since it required to have a deeper (and more complex) hierarchy of elements. 

Anyways, I added changes for these cases now as well (mainly ```src/compiler/compile/render_dom/wrappers/EachBlock.ts``` and ```src/compiler/compile/render_dom/wrappers/IfBlock.ts```).

By the way: since #4569 seemed very related to this one, I also tried the test case which is provided in there. It seems like that would be fixed as well. @rohanrichards: This is the [link to the fork](https://github.com/christianheine/svelte), in case you also want to give this a spin.

As before: Any feedback from someone with more experience on the compiler on how they feel about the changes would be highly appreciated. Many thanks in advance! 
Once I got more feedback I will try to create some decent test cases that cover these two issues.

In the meantime I will also continue use my fork for active development to see if anything else might be missing. 
yeah component fragments do not receive the `remount` prop, this is the correct fix for the current implementation

the real problem is that while local state in components is updating surgically, state in fragments is poorly managed and in most cases svelte simply pulls the old react on you & remounts everything as if the block was only just created

I'll try to address this problem & #4699 this week
In the meantime thumbs up for this fix, here is a test for the issue  https://github.com/sveltejs/svelte/commit/08b3abb80dd8e1e99066ac6a637b085ef2003633

this issue is unique to keyed each blocks as it is the only way in svelte to reorder nodes dynamically
there is no remount going on in unkeyed blocks as those have their props changed instead

eventually I believe that actions should not be recalled on mere order change
but in the meantime this is potentially a huge memory leak depending on the usecase 
Ran into this issue today. Thanks for working on the fix!

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@
 * Add `a11y-no-onchange` warning ([#4788](https://github.com/sveltejs/svelte/pull/4788))
 * Add `muted` binding for media elements ([#2998](https://github.com/sveltejs/svelte/issues/2998))
 * Fix let-less `<slot>` with context overflow ([#4624](https://github.com/sveltejs/svelte/issues/4624))
+* Fix `use:` actions being recreated when a keyed `{#each}` is reordered ([#4693](https://github.com/sveltejs/svelte/issues/4693))
 
 ## 3.22.3
 
diff --git a/src/compiler/compile/render_dom/Block.ts b/src/compiler/compile/render_dom/Block.ts
--- a/src/compiler/compile/render_dom/Block.ts
+++ b/src/compiler/compile/render_dom/Block.ts
@@ -299,7 +299,7 @@ export default class Block {
 				${this.chunks.mount}
 			}`;
 		} else {
-			properties.mount = x`function #mount(#target, #anchor, #remount) {
+			properties.mount = x`function #mount(#target, #anchor) {
 				${this.chunks.mount}
 			}`;
 		}
@@ -452,6 +452,9 @@ export default class Block {
 
 	render_listeners(chunk: string = '') {
 		if (this.event_listeners.length > 0) {
+			this.add_variable({ type: 'Identifier', name: '#mounted' });
+			this.chunks.destroy.push(b`#mounted = false`);
+
 			const dispose: Identifier = {
 				type: 'Identifier',
 				name: `#dispose${chunk}`
@@ -462,8 +465,10 @@ export default class Block {
 			if (this.event_listeners.length === 1) {
 				this.chunks.mount.push(
 					b`
-						if (#remount) ${dispose}();
-						${dispose} = ${this.event_listeners[0]};
+						if (!#mounted) {
+							${dispose} = ${this.event_listeners[0]};
+							#mounted = true;
+						}
 					`
 				);
 
@@ -472,10 +477,12 @@ export default class Block {
 				);
 			} else {
 				this.chunks.mount.push(b`
-					if (#remount) @run_all(${dispose});
-					${dispose} = [
-						${this.event_listeners}
-					];
+					if (!#mounted) {
+						${dispose} = [
+							${this.event_listeners}
+						];
+						#mounted = true;
+					}
 				`);
 
 				this.chunks.destroy.push(
diff --git a/src/runtime/internal/keyed_each.ts b/src/runtime/internal/keyed_each.ts
--- a/src/runtime/internal/keyed_each.ts
+++ b/src/runtime/internal/keyed_each.ts
@@ -56,7 +56,7 @@ export function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list
 
 	function insert(block) {
 		transition_in(block, 1);
-		block.m(node, next, lookup.has(block.key));
+		block.m(node, next);
 		lookup.set(block.key, block);
 		next = block.first;
 		n--;


Test patch:
diff --git a/test/js/samples/action-custom-event-handler/expected.js b/test/js/samples/action-custom-event-handler/expected.js
--- a/test/js/samples/action-custom-event-handler/expected.js
+++ b/test/js/samples/action-custom-event-handler/expected.js
@@ -14,6 +14,7 @@ import {
 function create_fragment(ctx) {
 	let button;
 	let foo_action;
+	let mounted;
 	let dispose;
 
 	return {
@@ -21,10 +22,13 @@ function create_fragment(ctx) {
 			button = element("button");
 			button.textContent = "foo";
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, button, anchor);
-			if (remount) dispose();
-			dispose = action_destroyer(foo_action = foo.call(null, button, /*foo_function*/ ctx[1]));
+
+			if (!mounted) {
+				dispose = action_destroyer(foo_action = foo.call(null, button, /*foo_function*/ ctx[1]));
+				mounted = true;
+			}
 		},
 		p(ctx, [dirty]) {
 			if (foo_action && is_function(foo_action.update) && dirty & /*bar*/ 1) foo_action.update.call(null, /*foo_function*/ ctx[1]);
@@ -33,6 +37,7 @@ function create_fragment(ctx) {
 		o: noop,
 		d(detaching) {
 			if (detaching) detach(button);
+			mounted = false;
 			dispose();
 		}
 	};
diff --git a/test/js/samples/action/expected.js b/test/js/samples/action/expected.js
--- a/test/js/samples/action/expected.js
+++ b/test/js/samples/action/expected.js
@@ -14,6 +14,7 @@ import {
 function create_fragment(ctx) {
 	let a;
 	let link_action;
+	let mounted;
 	let dispose;
 
 	return {
@@ -22,16 +23,20 @@ function create_fragment(ctx) {
 			a.textContent = "Test";
 			attr(a, "href", "#");
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, a, anchor);
-			if (remount) dispose();
-			dispose = action_destroyer(link_action = link.call(null, a));
+
+			if (!mounted) {
+				dispose = action_destroyer(link_action = link.call(null, a));
+				mounted = true;
+			}
 		},
 		p: noop,
 		i: noop,
 		o: noop,
 		d(detaching) {
 			if (detaching) detach(a);
+			mounted = false;
 			dispose();
 		}
 	};
diff --git a/test/js/samples/bind-online/expected.js b/test/js/samples/bind-online/expected.js
--- a/test/js/samples/bind-online/expected.js
+++ b/test/js/samples/bind-online/expected.js
@@ -10,23 +10,27 @@ import {
 } from "svelte/internal";
 
 function create_fragment(ctx) {
+	let mounted;
 	let dispose;
 	add_render_callback(/*onlinestatuschanged*/ ctx[1]);
 
 	return {
 		c: noop,
-		m(target, anchor, remount) {
-			if (remount) run_all(dispose);
-
-			dispose = [
-				listen(window, "online", /*onlinestatuschanged*/ ctx[1]),
-				listen(window, "offline", /*onlinestatuschanged*/ ctx[1])
-			];
+		m(target, anchor) {
+			if (!mounted) {
+				dispose = [
+					listen(window, "online", /*onlinestatuschanged*/ ctx[1]),
+					listen(window, "offline", /*onlinestatuschanged*/ ctx[1])
+				];
+
+				mounted = true;
+			}
 		},
 		p: noop,
 		i: noop,
 		o: noop,
 		d(detaching) {
+			mounted = false;
 			run_all(dispose);
 		}
 	};
diff --git a/test/js/samples/bind-open/expected.js b/test/js/samples/bind-open/expected.js
--- a/test/js/samples/bind-open/expected.js
+++ b/test/js/samples/bind-open/expected.js
@@ -12,6 +12,7 @@ import {
 
 function create_fragment(ctx) {
 	let details;
+	let mounted;
 	let dispose;
 
 	return {
@@ -21,11 +22,14 @@ function create_fragment(ctx) {
 			details.innerHTML = `<summary>summary</summary>content
 `;
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, details, anchor);
 			details.open = /*open*/ ctx[0];
-			if (remount) dispose();
-			dispose = listen(details, "toggle", /*details_toggle_handler*/ ctx[1]);
+
+			if (!mounted) {
+				dispose = listen(details, "toggle", /*details_toggle_handler*/ ctx[1]);
+				mounted = true;
+			}
 		},
 		p(ctx, [dirty]) {
 			if (dirty & /*open*/ 1) {
@@ -36,6 +40,7 @@ function create_fragment(ctx) {
 		o: noop,
 		d(detaching) {
 			if (detaching) detach(details);
+			mounted = false;
 			dispose();
 		}
 	};
diff --git a/test/js/samples/bindings-readonly-order/expected.js b/test/js/samples/bindings-readonly-order/expected.js
--- a/test/js/samples/bindings-readonly-order/expected.js
+++ b/test/js/samples/bindings-readonly-order/expected.js
@@ -17,6 +17,7 @@ function create_fragment(ctx) {
 	let input0;
 	let t;
 	let input1;
+	let mounted;
 	let dispose;
 
 	return {
@@ -27,16 +28,19 @@ function create_fragment(ctx) {
 			attr(input0, "type", "file");
 			attr(input1, "type", "file");
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, input0, anchor);
 			insert(target, t, anchor);
 			insert(target, input1, anchor);
-			if (remount) run_all(dispose);
 
-			dispose = [
-				listen(input0, "change", /*input0_change_handler*/ ctx[1]),
-				listen(input1, "change", /*input1_change_handler*/ ctx[2])
-			];
+			if (!mounted) {
+				dispose = [
+					listen(input0, "change", /*input0_change_handler*/ ctx[1]),
+					listen(input1, "change", /*input1_change_handler*/ ctx[2])
+				];
+
+				mounted = true;
+			}
 		},
 		p: noop,
 		i: noop,
@@ -45,6 +49,7 @@ function create_fragment(ctx) {
 			if (detaching) detach(input0);
 			if (detaching) detach(t);
 			if (detaching) detach(input1);
+			mounted = false;
 			run_all(dispose);
 		}
 	};
diff --git a/test/js/samples/capture-inject-dev-only/expected.js b/test/js/samples/capture-inject-dev-only/expected.js
--- a/test/js/samples/capture-inject-dev-only/expected.js
+++ b/test/js/samples/capture-inject-dev-only/expected.js
@@ -20,6 +20,7 @@ function create_fragment(ctx) {
 	let t0;
 	let t1;
 	let input;
+	let mounted;
 	let dispose;
 
 	return {
@@ -29,14 +30,17 @@ function create_fragment(ctx) {
 			t1 = space();
 			input = element("input");
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, p, anchor);
 			append(p, t0);
 			insert(target, t1, anchor);
 			insert(target, input, anchor);
 			set_input_value(input, /*foo*/ ctx[0]);
-			if (remount) dispose();
-			dispose = listen(input, "input", /*input_input_handler*/ ctx[1]);
+
+			if (!mounted) {
+				dispose = listen(input, "input", /*input_input_handler*/ ctx[1]);
+				mounted = true;
+			}
 		},
 		p(ctx, [dirty]) {
 			if (dirty & /*foo*/ 1) set_data(t0, /*foo*/ ctx[0]);
@@ -51,6 +55,7 @@ function create_fragment(ctx) {
 			if (detaching) detach(p);
 			if (detaching) detach(t1);
 			if (detaching) detach(input);
+			mounted = false;
 			dispose();
 		}
 	};
diff --git a/test/js/samples/component-static-var/expected.js b/test/js/samples/component-static-var/expected.js
--- a/test/js/samples/component-static-var/expected.js
+++ b/test/js/samples/component-static-var/expected.js
@@ -24,6 +24,7 @@ function create_fragment(ctx) {
 	let t1;
 	let input;
 	let current;
+	let mounted;
 	let dispose;
 	const foo = new Foo({ props: { x: y } });
 	const bar = new Bar({ props: { x: /*z*/ ctx[0] } });
@@ -36,7 +37,7 @@ function create_fragment(ctx) {
 			t1 = space();
 			input = element("input");
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			mount_component(foo, target, anchor);
 			insert(target, t0, anchor);
 			mount_component(bar, target, anchor);
@@ -44,8 +45,11 @@ function create_fragment(ctx) {
 			insert(target, input, anchor);
 			set_input_value(input, /*z*/ ctx[0]);
 			current = true;
-			if (remount) dispose();
-			dispose = listen(input, "input", /*input_input_handler*/ ctx[1]);
+
+			if (!mounted) {
+				dispose = listen(input, "input", /*input_input_handler*/ ctx[1]);
+				mounted = true;
+			}
 		},
 		p(ctx, [dirty]) {
 			const bar_changes = {};
@@ -73,6 +77,7 @@ function create_fragment(ctx) {
 			destroy_component(bar, detaching);
 			if (detaching) detach(t1);
 			if (detaching) detach(input);
+			mounted = false;
 			dispose();
 		}
 	};
diff --git a/test/js/samples/component-store-reassign-invalidate/expected.js b/test/js/samples/component-store-reassign-invalidate/expected.js
--- a/test/js/samples/component-store-reassign-invalidate/expected.js
+++ b/test/js/samples/component-store-reassign-invalidate/expected.js
@@ -22,6 +22,7 @@ function create_fragment(ctx) {
 	let t0;
 	let t1;
 	let button;
+	let mounted;
 	let dispose;
 
 	return {
@@ -32,13 +33,16 @@ function create_fragment(ctx) {
 			button = element("button");
 			button.textContent = "reset";
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, h1, anchor);
 			append(h1, t0);
 			insert(target, t1, anchor);
 			insert(target, button, anchor);
-			if (remount) dispose();
-			dispose = listen(button, "click", /*click_handler*/ ctx[2]);
+
+			if (!mounted) {
+				dispose = listen(button, "click", /*click_handler*/ ctx[2]);
+				mounted = true;
+			}
 		},
 		p(ctx, [dirty]) {
 			if (dirty & /*$foo*/ 2) set_data(t0, /*$foo*/ ctx[1]);
@@ -49,6 +53,7 @@ function create_fragment(ctx) {
 			if (detaching) detach(h1);
 			if (detaching) detach(t1);
 			if (detaching) detach(button);
+			mounted = false;
 			dispose();
 		}
 	};
diff --git a/test/js/samples/dont-invalidate-this/expected.js b/test/js/samples/dont-invalidate-this/expected.js
--- a/test/js/samples/dont-invalidate-this/expected.js
+++ b/test/js/samples/dont-invalidate-this/expected.js
@@ -12,22 +12,27 @@ import {
 
 function create_fragment(ctx) {
 	let input;
+	let mounted;
 	let dispose;
 
 	return {
 		c() {
 			input = element("input");
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, input, anchor);
-			if (remount) dispose();
-			dispose = listen(input, "input", make_uppercase);
+
+			if (!mounted) {
+				dispose = listen(input, "input", make_uppercase);
+				mounted = true;
+			}
 		},
 		p: noop,
 		i: noop,
 		o: noop,
 		d(detaching) {
 			if (detaching) detach(input);
+			mounted = false;
 			dispose();
 		}
 	};
diff --git a/test/js/samples/event-handler-dynamic/expected.js b/test/js/samples/event-handler-dynamic/expected.js
--- a/test/js/samples/event-handler-dynamic/expected.js
+++ b/test/js/samples/event-handler-dynamic/expected.js
@@ -26,6 +26,7 @@ function create_fragment(ctx) {
 	let t4;
 	let t5;
 	let button2;
+	let mounted;
 	let dispose;
 
 	return {
@@ -43,7 +44,7 @@ function create_fragment(ctx) {
 			button2 = element("button");
 			button2.textContent = "click";
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, p0, anchor);
 			append(p0, button0);
 			append(p0, t1);
@@ -53,15 +54,18 @@ function create_fragment(ctx) {
 			append(p1, t4);
 			insert(target, t5, anchor);
 			insert(target, button2, anchor);
-			if (remount) run_all(dispose);
 
-			dispose = [
-				listen(button0, "click", /*updateHandler1*/ ctx[2]),
-				listen(button1, "click", /*updateHandler2*/ ctx[3]),
-				listen(button2, "click", function () {
-					if (is_function(/*clickHandler*/ ctx[0])) /*clickHandler*/ ctx[0].apply(this, arguments);
-				})
-			];
+			if (!mounted) {
+				dispose = [
+					listen(button0, "click", /*updateHandler1*/ ctx[2]),
+					listen(button1, "click", /*updateHandler2*/ ctx[3]),
+					listen(button2, "click", function () {
+						if (is_function(/*clickHandler*/ ctx[0])) /*clickHandler*/ ctx[0].apply(this, arguments);
+					})
+				];
+
+				mounted = true;
+			}
 		},
 		p(new_ctx, [dirty]) {
 			ctx = new_ctx;
@@ -75,6 +79,7 @@ function create_fragment(ctx) {
 			if (detaching) detach(p1);
 			if (detaching) detach(t5);
 			if (detaching) detach(button2);
+			mounted = false;
 			run_all(dispose);
 		}
 	};
diff --git a/test/js/samples/event-handler-no-passive/expected.js b/test/js/samples/event-handler-no-passive/expected.js
--- a/test/js/samples/event-handler-no-passive/expected.js
+++ b/test/js/samples/event-handler-no-passive/expected.js
@@ -13,6 +13,7 @@ import {
 
 function create_fragment(ctx) {
 	let a;
+	let mounted;
 	let dispose;
 
 	return {
@@ -21,16 +22,20 @@ function create_fragment(ctx) {
 			a.textContent = "this should not navigate to example.com";
 			attr(a, "href", "https://example.com");
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, a, anchor);
-			if (remount) dispose();
-			dispose = listen(a, "touchstart", touchstart_handler);
+
+			if (!mounted) {
+				dispose = listen(a, "touchstart", touchstart_handler);
+				mounted = true;
+			}
 		},
 		p: noop,
 		i: noop,
 		o: noop,
 		d(detaching) {
 			if (detaching) detach(a);
+			mounted = false;
 			dispose();
 		}
 	};
diff --git a/test/js/samples/event-modifiers/expected.js b/test/js/samples/event-modifiers/expected.js
--- a/test/js/samples/event-modifiers/expected.js
+++ b/test/js/samples/event-modifiers/expected.js
@@ -22,6 +22,7 @@ function create_fragment(ctx) {
 	let button1;
 	let t3;
 	let button2;
+	let mounted;
 	let dispose;
 
 	return {
@@ -36,27 +37,31 @@ function create_fragment(ctx) {
 			button2 = element("button");
 			button2.textContent = "or me!";
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, div, anchor);
 			append(div, button0);
 			append(div, t1);
 			append(div, button1);
 			append(div, t3);
 			append(div, button2);
-			if (remount) run_all(dispose);
 
-			dispose = [
-				listen(button0, "click", stop_propagation(prevent_default(handleClick))),
-				listen(button1, "click", handleClick, { once: true, capture: true }),
-				listen(button2, "click", handleClick, true),
-				listen(div, "touchstart", handleTouchstart, { passive: true })
-			];
+			if (!mounted) {
+				dispose = [
+					listen(button0, "click", stop_propagation(prevent_default(handleClick))),
+					listen(button1, "click", handleClick, { once: true, capture: true }),
+					listen(button2, "click", handleClick, true),
+					listen(div, "touchstart", handleTouchstart, { passive: true })
+				];
+
+				mounted = true;
+			}
 		},
 		p: noop,
 		i: noop,
 		o: noop,
 		d(detaching) {
 			if (detaching) detach(div);
+			mounted = false;
 			run_all(dispose);
 		}
 	};
diff --git a/test/js/samples/input-files/expected.js b/test/js/samples/input-files/expected.js
--- a/test/js/samples/input-files/expected.js
+++ b/test/js/samples/input-files/expected.js
@@ -13,6 +13,7 @@ import {
 
 function create_fragment(ctx) {
 	let input;
+	let mounted;
 	let dispose;
 
 	return {
@@ -21,16 +22,20 @@ function create_fragment(ctx) {
 			attr(input, "type", "file");
 			input.multiple = true;
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, input, anchor);
-			if (remount) dispose();
-			dispose = listen(input, "change", /*input_change_handler*/ ctx[1]);
+
+			if (!mounted) {
+				dispose = listen(input, "change", /*input_change_handler*/ ctx[1]);
+				mounted = true;
+			}
 		},
 		p: noop,
 		i: noop,
 		o: noop,
 		d(detaching) {
 			if (detaching) detach(input);
+			mounted = false;
 			dispose();
 		}
 	};
diff --git a/test/js/samples/input-no-initial-value/expected.js b/test/js/samples/input-no-initial-value/expected.js
--- a/test/js/samples/input-no-initial-value/expected.js
+++ b/test/js/samples/input-no-initial-value/expected.js
@@ -20,6 +20,7 @@ function create_fragment(ctx) {
 	let input;
 	let t0;
 	let button;
+	let mounted;
 	let dispose;
 
 	return {
@@ -32,18 +33,21 @@ function create_fragment(ctx) {
 			attr(input, "type", "text");
 			input.required = true;
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, form, anchor);
 			append(form, input);
 			set_input_value(input, /*test*/ ctx[0]);
 			append(form, t0);
 			append(form, button);
-			if (remount) run_all(dispose);
 
-			dispose = [
-				listen(input, "input", /*input_input_handler*/ ctx[2]),
-				listen(form, "submit", /*handleSubmit*/ ctx[1])
-			];
+			if (!mounted) {
+				dispose = [
+					listen(input, "input", /*input_input_handler*/ ctx[2]),
+					listen(form, "submit", /*handleSubmit*/ ctx[1])
+				];
+
+				mounted = true;
+			}
 		},
 		p(ctx, [dirty]) {
 			if (dirty & /*test*/ 1 && input.value !== /*test*/ ctx[0]) {
@@ -54,6 +58,7 @@ function create_fragment(ctx) {
 		o: noop,
 		d(detaching) {
 			if (detaching) detach(form);
+			mounted = false;
 			run_all(dispose);
 		}
 	};
diff --git a/test/js/samples/input-range/expected.js b/test/js/samples/input-range/expected.js
--- a/test/js/samples/input-range/expected.js
+++ b/test/js/samples/input-range/expected.js
@@ -16,6 +16,7 @@ import {
 
 function create_fragment(ctx) {
 	let input;
+	let mounted;
 	let dispose;
 
 	return {
@@ -23,15 +24,18 @@ function create_fragment(ctx) {
 			input = element("input");
 			attr(input, "type", "range");
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, input, anchor);
 			set_input_value(input, /*value*/ ctx[0]);
-			if (remount) run_all(dispose);
 
-			dispose = [
-				listen(input, "change", /*input_change_input_handler*/ ctx[1]),
-				listen(input, "input", /*input_change_input_handler*/ ctx[1])
-			];
+			if (!mounted) {
+				dispose = [
+					listen(input, "change", /*input_change_input_handler*/ ctx[1]),
+					listen(input, "input", /*input_change_input_handler*/ ctx[1])
+				];
+
+				mounted = true;
+			}
 		},
 		p(ctx, [dirty]) {
 			if (dirty & /*value*/ 1) {
@@ -42,6 +46,7 @@ function create_fragment(ctx) {
 		o: noop,
 		d(detaching) {
 			if (detaching) detach(input);
+			mounted = false;
 			run_all(dispose);
 		}
 	};
diff --git a/test/js/samples/input-value/expected.js b/test/js/samples/input-value/expected.js
--- a/test/js/samples/input-value/expected.js
+++ b/test/js/samples/input-value/expected.js
@@ -20,6 +20,7 @@ function create_fragment(ctx) {
 	let h1;
 	let t1;
 	let t2;
+	let mounted;
 	let dispose;
 
 	return {
@@ -31,14 +32,17 @@ function create_fragment(ctx) {
 			t2 = text("!");
 			input.value = /*name*/ ctx[0];
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, input, anchor);
 			insert(target, t0, anchor);
 			insert(target, h1, anchor);
 			append(h1, t1);
 			append(h1, t2);
-			if (remount) dispose();
-			dispose = listen(input, "input", /*onInput*/ ctx[1]);
+
+			if (!mounted) {
+				dispose = listen(input, "input", /*onInput*/ ctx[1]);
+				mounted = true;
+			}
 		},
 		p(ctx, [dirty]) {
 			if (dirty & /*name*/ 1 && input.value !== /*name*/ ctx[0]) {
@@ -53,6 +57,7 @@ function create_fragment(ctx) {
 			if (detaching) detach(input);
 			if (detaching) detach(t0);
 			if (detaching) detach(h1);
+			mounted = false;
 			dispose();
 		}
 	};
diff --git a/test/js/samples/input-without-blowback-guard/expected.js b/test/js/samples/input-without-blowback-guard/expected.js
--- a/test/js/samples/input-without-blowback-guard/expected.js
+++ b/test/js/samples/input-without-blowback-guard/expected.js
@@ -13,6 +13,7 @@ import {
 
 function create_fragment(ctx) {
 	let input;
+	let mounted;
 	let dispose;
 
 	return {
@@ -20,11 +21,14 @@ function create_fragment(ctx) {
 			input = element("input");
 			attr(input, "type", "checkbox");
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, input, anchor);
 			input.checked = /*foo*/ ctx[0];
-			if (remount) dispose();
-			dispose = listen(input, "change", /*input_change_handler*/ ctx[1]);
+
+			if (!mounted) {
+				dispose = listen(input, "change", /*input_change_handler*/ ctx[1]);
+				mounted = true;
+			}
 		},
 		p(ctx, [dirty]) {
 			if (dirty & /*foo*/ 1) {
@@ -35,6 +39,7 @@ function create_fragment(ctx) {
 		o: noop,
 		d(detaching) {
 			if (detaching) detach(input);
+			mounted = false;
 			dispose();
 		}
 	};
diff --git a/test/js/samples/instrumentation-script-if-no-block/expected.js b/test/js/samples/instrumentation-script-if-no-block/expected.js
--- a/test/js/samples/instrumentation-script-if-no-block/expected.js
+++ b/test/js/samples/instrumentation-script-if-no-block/expected.js
@@ -20,6 +20,7 @@ function create_fragment(ctx) {
 	let p;
 	let t2;
 	let t3;
+	let mounted;
 	let dispose;
 
 	return {
@@ -31,14 +32,17 @@ function create_fragment(ctx) {
 			t2 = text("x: ");
 			t3 = text(/*x*/ ctx[0]);
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, button, anchor);
 			insert(target, t1, anchor);
 			insert(target, p, anchor);
 			append(p, t2);
 			append(p, t3);
-			if (remount) dispose();
-			dispose = listen(button, "click", /*foo*/ ctx[1]);
+
+			if (!mounted) {
+				dispose = listen(button, "click", /*foo*/ ctx[1]);
+				mounted = true;
+			}
 		},
 		p(ctx, [dirty]) {
 			if (dirty & /*x*/ 1) set_data(t3, /*x*/ ctx[0]);
@@ -49,6 +53,7 @@ function create_fragment(ctx) {
 			if (detaching) detach(button);
 			if (detaching) detach(t1);
 			if (detaching) detach(p);
+			mounted = false;
 			dispose();
 		}
 	};
diff --git a/test/js/samples/instrumentation-script-x-equals-x/expected.js b/test/js/samples/instrumentation-script-x-equals-x/expected.js
--- a/test/js/samples/instrumentation-script-x-equals-x/expected.js
+++ b/test/js/samples/instrumentation-script-x-equals-x/expected.js
@@ -21,6 +21,7 @@ function create_fragment(ctx) {
 	let t2;
 	let t3_value = /*things*/ ctx[0].length + "";
 	let t3;
+	let mounted;
 	let dispose;
 
 	return {
@@ -32,14 +33,17 @@ function create_fragment(ctx) {
 			t2 = text("number of things: ");
 			t3 = text(t3_value);
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, button, anchor);
 			insert(target, t1, anchor);
 			insert(target, p, anchor);
 			append(p, t2);
 			append(p, t3);
-			if (remount) dispose();
-			dispose = listen(button, "click", /*foo*/ ctx[1]);
+
+			if (!mounted) {
+				dispose = listen(button, "click", /*foo*/ ctx[1]);
+				mounted = true;
+			}
 		},
 		p(ctx, [dirty]) {
 			if (dirty & /*things*/ 1 && t3_value !== (t3_value = /*things*/ ctx[0].length + "")) set_data(t3, t3_value);
@@ -50,6 +54,7 @@ function create_fragment(ctx) {
 			if (detaching) detach(button);
 			if (detaching) detach(t1);
 			if (detaching) detach(p);
+			mounted = false;
 			dispose();
 		}
 	};
diff --git a/test/js/samples/instrumentation-template-if-no-block/expected.js b/test/js/samples/instrumentation-template-if-no-block/expected.js
--- a/test/js/samples/instrumentation-template-if-no-block/expected.js
+++ b/test/js/samples/instrumentation-template-if-no-block/expected.js
@@ -20,6 +20,7 @@ function create_fragment(ctx) {
 	let p;
 	let t2;
 	let t3;
+	let mounted;
 	let dispose;
 
 	return {
@@ -31,14 +32,17 @@ function create_fragment(ctx) {
 			t2 = text("x: ");
 			t3 = text(/*x*/ ctx[0]);
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, button, anchor);
 			insert(target, t1, anchor);
 			insert(target, p, anchor);
 			append(p, t2);
 			append(p, t3);
-			if (remount) dispose();
-			dispose = listen(button, "click", /*click_handler*/ ctx[1]);
+
+			if (!mounted) {
+				dispose = listen(button, "click", /*click_handler*/ ctx[1]);
+				mounted = true;
+			}
 		},
 		p(ctx, [dirty]) {
 			if (dirty & /*x*/ 1) set_data(t3, /*x*/ ctx[0]);
@@ -49,6 +53,7 @@ function create_fragment(ctx) {
 			if (detaching) detach(button);
 			if (detaching) detach(t1);
 			if (detaching) detach(p);
+			mounted = false;
 			dispose();
 		}
 	};
diff --git a/test/js/samples/instrumentation-template-x-equals-x/expected.js b/test/js/samples/instrumentation-template-x-equals-x/expected.js
--- a/test/js/samples/instrumentation-template-x-equals-x/expected.js
+++ b/test/js/samples/instrumentation-template-x-equals-x/expected.js
@@ -21,6 +21,7 @@ function create_fragment(ctx) {
 	let t2;
 	let t3_value = /*things*/ ctx[0].length + "";
 	let t3;
+	let mounted;
 	let dispose;
 
 	return {
@@ -32,14 +33,17 @@ function create_fragment(ctx) {
 			t2 = text("number of things: ");
 			t3 = text(t3_value);
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, button, anchor);
 			insert(target, t1, anchor);
 			insert(target, p, anchor);
 			append(p, t2);
 			append(p, t3);
-			if (remount) dispose();
-			dispose = listen(button, "click", /*click_handler*/ ctx[1]);
+
+			if (!mounted) {
+				dispose = listen(button, "click", /*click_handler*/ ctx[1]);
+				mounted = true;
+			}
 		},
 		p(ctx, [dirty]) {
 			if (dirty & /*things*/ 1 && t3_value !== (t3_value = /*things*/ ctx[0].length + "")) set_data(t3, t3_value);
@@ -50,6 +54,7 @@ function create_fragment(ctx) {
 			if (detaching) detach(button);
 			if (detaching) detach(t1);
 			if (detaching) detach(p);
+			mounted = false;
 			dispose();
 		}
 	};
diff --git a/test/js/samples/media-bindings/expected.js b/test/js/samples/media-bindings/expected.js
--- a/test/js/samples/media-bindings/expected.js
+++ b/test/js/samples/media-bindings/expected.js
@@ -19,6 +19,7 @@ function create_fragment(ctx) {
 	let audio_updating = false;
 	let audio_animationframe;
 	let audio_is_paused = true;
+	let mounted;
 	let dispose;
 
 	function audio_timeupdate_handler() {
@@ -42,7 +43,7 @@ function create_fragment(ctx) {
 			if (/*seeking*/ ctx[9] === void 0) add_render_callback(() => /*audio_seeking_seeked_handler*/ ctx[18].call(audio));
 			if (/*ended*/ ctx[10] === void 0) add_render_callback(() => /*audio_ended_handler*/ ctx[19].call(audio));
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, audio, anchor);
 
 			if (!isNaN(/*volume*/ ctx[6])) {
@@ -55,21 +56,23 @@ function create_fragment(ctx) {
 				audio.playbackRate = /*playbackRate*/ ctx[8];
 			}
 
-			if (remount) run_all(dispose);
-
-			dispose = [
-				listen(audio, "progress", /*audio_progress_handler*/ ctx[11]),
-				listen(audio, "loadedmetadata", /*audio_loadedmetadata_handler*/ ctx[12]),
-				listen(audio, "timeupdate", audio_timeupdate_handler),
-				listen(audio, "durationchange", /*audio_durationchange_handler*/ ctx[14]),
-				listen(audio, "play", /*audio_play_pause_handler*/ ctx[15]),
-				listen(audio, "pause", /*audio_play_pause_handler*/ ctx[15]),
-				listen(audio, "volumechange", /*audio_volumechange_handler*/ ctx[16]),
-				listen(audio, "ratechange", /*audio_ratechange_handler*/ ctx[17]),
-				listen(audio, "seeking", /*audio_seeking_seeked_handler*/ ctx[18]),
-				listen(audio, "seeked", /*audio_seeking_seeked_handler*/ ctx[18]),
-				listen(audio, "ended", /*audio_ended_handler*/ ctx[19])
-			];
+			if (!mounted) {
+				dispose = [
+					listen(audio, "progress", /*audio_progress_handler*/ ctx[11]),
+					listen(audio, "loadedmetadata", /*audio_loadedmetadata_handler*/ ctx[12]),
+					listen(audio, "timeupdate", audio_timeupdate_handler),
+					listen(audio, "durationchange", /*audio_durationchange_handler*/ ctx[14]),
+					listen(audio, "play", /*audio_play_pause_handler*/ ctx[15]),
+					listen(audio, "pause", /*audio_play_pause_handler*/ ctx[15]),
+					listen(audio, "volumechange", /*audio_volumechange_handler*/ ctx[16]),
+					listen(audio, "ratechange", /*audio_ratechange_handler*/ ctx[17]),
+					listen(audio, "seeking", /*audio_seeking_seeked_handler*/ ctx[18]),
+					listen(audio, "seeked", /*audio_seeking_seeked_handler*/ ctx[18]),
+					listen(audio, "ended", /*audio_ended_handler*/ ctx[19])
+				];
+
+				mounted = true;
+			}
 		},
 		p(ctx, [dirty]) {
 			if (!audio_updating && dirty & /*currentTime*/ 8 && !isNaN(/*currentTime*/ ctx[3])) {
@@ -98,6 +101,7 @@ function create_fragment(ctx) {
 		o: noop,
 		d(detaching) {
 			if (detaching) detach(audio);
+			mounted = false;
 			run_all(dispose);
 		}
 	};
diff --git a/test/js/samples/video-bindings/expected.js b/test/js/samples/video-bindings/expected.js
--- a/test/js/samples/video-bindings/expected.js
+++ b/test/js/samples/video-bindings/expected.js
@@ -19,6 +19,7 @@ function create_fragment(ctx) {
 	let video_updating = false;
 	let video_animationframe;
 	let video_resize_listener;
+	let mounted;
 	let dispose;
 
 	function video_timeupdate_handler() {
@@ -38,15 +39,18 @@ function create_fragment(ctx) {
 			if (/*videoHeight*/ ctx[1] === void 0 || /*videoWidth*/ ctx[2] === void 0) add_render_callback(() => /*video_resize_handler*/ ctx[5].call(video));
 			add_render_callback(() => /*video_elementresize_handler*/ ctx[6].call(video));
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, video, anchor);
 			video_resize_listener = add_resize_listener(video, /*video_elementresize_handler*/ ctx[6].bind(video));
-			if (remount) run_all(dispose);
 
-			dispose = [
-				listen(video, "timeupdate", video_timeupdate_handler),
-				listen(video, "resize", /*video_resize_handler*/ ctx[5])
-			];
+			if (!mounted) {
+				dispose = [
+					listen(video, "timeupdate", video_timeupdate_handler),
+					listen(video, "resize", /*video_resize_handler*/ ctx[5])
+				];
+
+				mounted = true;
+			}
 		},
 		p(ctx, [dirty]) {
 			if (!video_updating && dirty & /*currentTime*/ 1 && !isNaN(/*currentTime*/ ctx[0])) {
@@ -60,6 +64,7 @@ function create_fragment(ctx) {
 		d(detaching) {
 			if (detaching) detach(video);
 			video_resize_listener();
+			mounted = false;
 			run_all(dispose);
 		}
 	};
diff --git a/test/js/samples/window-binding-online/expected.js b/test/js/samples/window-binding-online/expected.js
--- a/test/js/samples/window-binding-online/expected.js
+++ b/test/js/samples/window-binding-online/expected.js
@@ -10,23 +10,27 @@ import {
 } from "svelte/internal";
 
 function create_fragment(ctx) {
+	let mounted;
 	let dispose;
 	add_render_callback(/*onlinestatuschanged*/ ctx[1]);
 
 	return {
 		c: noop,
-		m(target, anchor, remount) {
-			if (remount) run_all(dispose);
-
-			dispose = [
-				listen(window, "online", /*onlinestatuschanged*/ ctx[1]),
-				listen(window, "offline", /*onlinestatuschanged*/ ctx[1])
-			];
+		m(target, anchor) {
+			if (!mounted) {
+				dispose = [
+					listen(window, "online", /*onlinestatuschanged*/ ctx[1]),
+					listen(window, "offline", /*onlinestatuschanged*/ ctx[1])
+				];
+
+				mounted = true;
+			}
 		},
 		p: noop,
 		i: noop,
 		o: noop,
 		d(detaching) {
+			mounted = false;
 			run_all(dispose);
 		}
 	};
diff --git a/test/js/samples/window-binding-scroll/expected.js b/test/js/samples/window-binding-scroll/expected.js
--- a/test/js/samples/window-binding-scroll/expected.js
+++ b/test/js/samples/window-binding-scroll/expected.js
@@ -25,6 +25,7 @@ function create_fragment(ctx) {
 	let p;
 	let t0;
 	let t1;
+	let mounted;
 	let dispose;
 	add_render_callback(/*onwindowscroll*/ ctx[1]);
 
@@ -34,18 +35,21 @@ function create_fragment(ctx) {
 			t0 = text("scrolled to ");
 			t1 = text(/*y*/ ctx[0]);
 		},
-		m(target, anchor, remount) {
+		m(target, anchor) {
 			insert(target, p, anchor);
 			append(p, t0);
 			append(p, t1);
-			if (remount) dispose();
 
-			dispose = listen(window, "scroll", () => {
-				scrolling = true;
-				clearTimeout(scrolling_timeout);
-				scrolling_timeout = setTimeout(clear_scrolling, 100);
-				/*onwindowscroll*/ ctx[1]();
-			});
+			if (!mounted) {
+				dispose = listen(window, "scroll", () => {
+					scrolling = true;
+					clearTimeout(scrolling_timeout);
+					scrolling_timeout = setTimeout(clear_scrolling, 100);
+					/*onwindowscroll*/ ctx[1]();
+				});
+
+				mounted = true;
+			}
 		},
 		p(ctx, [dirty]) {
 			if (dirty & /*y*/ 1 && !scrolling) {
@@ -61,6 +65,7 @@ function create_fragment(ctx) {
 		o: noop,
 		d(detaching) {
 			if (detaching) detach(p);
+			mounted = false;
 			dispose();
 		}
 	};
diff --git a/test/runtime/samples/each-block-keyed-component-action/Component.svelte b/test/runtime/samples/each-block-keyed-component-action/Component.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-keyed-component-action/Component.svelte
@@ -0,0 +1,5 @@
+<script>
+	export let action;
+</script>
+
+<div use:action />
diff --git a/test/runtime/samples/each-block-keyed-component-action/_config.js b/test/runtime/samples/each-block-keyed-component-action/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-keyed-component-action/_config.js
@@ -0,0 +1,21 @@
+export default {
+	test({ assert, component, raf }) {
+		assert.equal(component.count, 0);
+
+		component.arr = ["2"];
+
+		assert.equal(component.count, 1);
+
+		component.arr = ["1", "2"];
+
+		assert.equal(component.count, 2);
+
+		component.arr = ["2", "1"];
+
+		assert.equal(component.count, 2);
+
+		component.arr = [];
+
+		assert.equal(component.count, 0);
+	},
+};
diff --git a/test/runtime/samples/each-block-keyed-component-action/main.svelte b/test/runtime/samples/each-block-keyed-component-action/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-keyed-component-action/main.svelte
@@ -0,0 +1,17 @@
+<script>
+	import Component from "./Component.svelte";
+	export let arr = [];
+	export let count = 0;
+	function action(node, params) {
+		count += 1;
+		return {
+			destroy() {
+				count -= 1;
+			}
+		};
+	}
+</script>
+
+{#each arr as item (item)}
+	<Component {action} />
+{/each}