Repository: sveltejs/svelte (JavaScript)
Date: 2021-01-02 04:16:43+00:00
Task type: Bug Fix

Issue:
DOM replacement breaks when a component child is in the middle of an out animation
**Describe the bug**
There's a race condition when you replace a component that has a child with an out transition that is currently running, svelte dies. 

In production, we see the old component stay on the page _in addition to_ the component replacing it, and we don't see any console errors on this (with svelte 3.29.4). 

In REPL there's an exception and JS dies.

**Logs**
`Uncaught (in promise): if_block.p is not a function` (only in REPL, version 3.31.0)

**To Reproduce**
https://svelte.dev/repl/46686d49624a4f099f738998fbe5301c?version=3.31.0

**Expected behavior**
Out transition of the child would be killed, the whole component would be replaced.  

**Information about your Svelte project:**

- Your browser and the version: (e.x. Chrome 52.1, Firefox 48.0, IE 10)
tested in chrome and safari

- Your operating system: (e.x. OS X 10, Ubuntu Linux 19.10, Windows XP, etc)
OS X

- Svelte version (Please check you can reproduce the issue with the latest release!)
3.29.4 and 3.31.0

- Whether your project uses Webpack or Rollup


**Severity**
How severe an issue is this bug to you? Is this annoying, blocking some users, blocking an upgrade or blocking your usage of Svelte entirely?

We can work around it (also shown in the REPL) but it's a real gotcha, especially with production code that has nested components.




Issue discussion:
I got the same error and found a fix for it. The problem is the `{:else}` block. If you split the code up into two separate if-blocks everything works as expected.

This is your example from above, but with the fix applied:
https://svelte.dev/repl/dbba868b5fa64be4b750b9296c6f5cfa?version=3.31.0

You can toggle really fast and nothing breaks
Good sleuthing!

In looking at our source code, it turns out that it wasn't an `{:else}` block that was causing problems -- it was an interval.

Here's an updated REPL that still breaks -- it looks like there's a race condition going on, where the onInterval is updating the `{#if}` block but for some reason the onDestroy isn't getting called when the parent is swapped out, so then it leaves everything in the DOM.

Should this be a different issue?

https://svelte.dev/repl/3defa9a68e7147ec8a18a4a15d422c36?version=3.29.4
related #5815

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 ## Unreleased
 
 * Rework SSR store handling to subscribe and unsubscribe as in DOM mode ([#3375](https://github.com/sveltejs/svelte/issues/3375), [#3582](https://github.com/sveltejs/svelte/issues/3582), [#3636](https://github.com/sveltejs/svelte/issues/3636))
+* Fix error when removing elements that are already transitioning out ([#5789](https://github.com/sveltejs/svelte/issues/5789), [#5808](https://github.com/sveltejs/svelte/issues/5808))
 
 ## 3.31.1
 
diff --git a/src/compiler/compile/render_dom/wrappers/IfBlock.ts b/src/compiler/compile/render_dom/wrappers/IfBlock.ts
--- a/src/compiler/compile/render_dom/wrappers/IfBlock.ts
+++ b/src/compiler/compile/render_dom/wrappers/IfBlock.ts
@@ -448,7 +448,7 @@ export default class IfBlockWrapper extends Wrapper {
 					${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#ctx);
 					${name}.c();
 				} else {
-					${name}.p(#ctx, #dirty);
+					${dynamic && b`${name}.p(#ctx, #dirty);`}
 				}
 				${has_transitions && b`@transition_in(${name}, 1);`}
 				${name}.m(${update_mount_node}, ${anchor});
@@ -472,10 +472,13 @@ export default class IfBlockWrapper extends Wrapper {
 					}
 				`;
 
+			block.chunks.update.push(b`
+				let ${previous_block_index} = ${current_block_type_index};
+				${current_block_type_index} = ${select_block_type}(#ctx, #dirty);
+			`);
+
 			if (dynamic) {
 				block.chunks.update.push(b`
-					let ${previous_block_index} = ${current_block_type_index};
-					${current_block_type_index} = ${select_block_type}(#ctx, #dirty);
 					if (${current_block_type_index} === ${previous_block_index}) {
 						${if_current_block_type_index(b`${if_blocks}[${current_block_type_index}].p(#ctx, #dirty);`)}
 					} else {
@@ -484,8 +487,6 @@ export default class IfBlockWrapper extends Wrapper {
 				`);
 			} else {
 				block.chunks.update.push(b`
-					let ${previous_block_index} = ${current_block_type_index};
-					${current_block_type_index} = ${select_block_type}(#ctx, #dirty);
 					if (${current_block_type_index} !== ${previous_block_index}) {
 						${change_block}
 					}


Test patch:
diff --git a/test/runtime/samples/transition-js-if-else-block-not-dynamic-outro/_config.js b/test/runtime/samples/transition-js-if-else-block-not-dynamic-outro/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/transition-js-if-else-block-not-dynamic-outro/_config.js
@@ -0,0 +1,38 @@
+export default {
+	async test({ assert, component, target, window, raf }) {
+		const t = target.querySelector('#t');
+
+		await (component.condition = false);
+
+		let time = 0;
+		raf.tick(time += 25);
+
+		assert.htmlEqual(target.innerHTML, `
+			<div id="t" foo="0.75">TRUE</div>
+			<div id="f">FALSE</div>
+		`);
+
+		// toggling back in the middle of the out transition
+		// will reuse the previous element
+		await (component.condition = true);
+
+		assert.htmlEqual(target.innerHTML, `
+			<div id="f">FALSE</div>
+			<div id="t" foo="1">TRUE</div>
+		`);
+		assert.equal(target.querySelector('#t'), t);
+
+		raf.tick(time += 25);
+
+		assert.htmlEqual(target.innerHTML, `
+			<div id="f" foo="0.75">FALSE</div>
+			<div id="t" foo="1">TRUE</div>
+		`);
+
+		raf.tick(time += 75);
+
+		assert.htmlEqual(target.innerHTML, `
+			<div id="t" foo="1">TRUE</div>
+		`);
+	}
+};
diff --git a/test/runtime/samples/transition-js-if-else-block-not-dynamic-outro/main.svelte b/test/runtime/samples/transition-js-if-else-block-not-dynamic-outro/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/transition-js-if-else-block-not-dynamic-outro/main.svelte
@@ -0,0 +1,18 @@
+<script>
+	export let condition = true;
+
+	function foo(node) {
+		return {
+			duration: 100,
+			tick: t => {
+				node.setAttribute('foo', t);
+			}
+		};
+	}
+</script>
+
+{#if condition}
+	<div id="t" out:foo>TRUE</div>
+{:else}
+	<div id="f" out:foo>FALSE</div>
+{/if}
\ No newline at end of file