Repository: sveltejs/svelte (JavaScript)
Date: 2019-07-09 18:06:37+00:00
Task type: Bug Fix

Issue:
Uncaught (in promise) TypeError: Cannot read property 'removeChild' of null
I couldn't reproduce the error in the repl and I also can't post the data since it is confidential.
I'll try to post as much information as possible though.

This is the component:
```
  {#each schueler as s (s.ID)}
    {#each s.abschnitte.filter(aktJahr) as a (a.ID)}
        <Voffset/>{a.Jahr}
    {/each}
  {/each}

<script>
  import Voffset from './partials/Voffset.html'
  // the file looks like this
  // ./partials/Voffset.html
  // <div></div>

  export let jahr, schueler
  // schueler looks like this just way more complex and about 1MB in size
  // schueler = [{abschnitte: [{Jahr: 2017},{Jahr: 2018}]}, {abschnitte: [{Jahr: 2017},{Jahr: 2018}]}]
  $: aktJahr = a => a.Jahr === jahr
</script>
```

```
svelte = new Component({ target: document.querySelector('svelte'), props })
```
this works just fine.

later on when I change some props I do it like this:

```
svelte.$set({ jahr: abschnitt.jahr, abschnitt: abschnitt.abschnitt })
```

sometimes it works, sometimes not. It renders the component but then fails and I can't change the data again.

This is the error message:
```
internal.mjs:111 Uncaught (in promise) TypeError: Cannot read property 'removeChild' of null
    at detachNode (VM110 bundle.js:113)
    at Object.destroy [as d] (VM110 bundle.js:1555)
    at destroyBlock (VM110 bundle.js:1006)
    at on_outro (VM110 bundle.js:1012)
    at run (VM110 bundle.js:23)
    at Array.forEach (<anonymous>)
    at run_all (VM110 bundle.js:31)
    at check_outros (VM110 bundle.js:535)
    at Object.update [as p] (VM110 bundle.js:1675)
    at update (VM110 bundle.js:937)
```
this seems to only happen with two nested loops and the imported component.


Issue discussion:
A very simple and well working workaround (not even a speed penalty so far) seems to be a `destroy()` and recreation of the component. Should have tried that earlier than setting the data.
I have also encountered this, but cannot reproduce at the moment. It went usually like this:

1. Visit `/items`
2. Click link to `/items/[id]/specific` (happens to be a sapper app)
3. Click link to `/items`
    => empty page and console shows: `TypeError: Cannot read property 'removeChild' of null`

Solution patch:
diff --git a/src/runtime/internal/transitions.ts b/src/runtime/internal/transitions.ts
--- a/src/runtime/internal/transitions.ts
+++ b/src/runtime/internal/transitions.ts
@@ -28,15 +28,17 @@ let outros;
 
 export function group_outros() {
 	outros = {
-		remaining: 0,
-		callbacks: []
+		r: 0,     // remaining outros
+		c: [],    // callbacks
+		p: outros // parent group
 	};
 }
 
 export function check_outros() {
-	if (!outros.remaining) {
-		run_all(outros.callbacks);
+	if (!outros.r) {
+		run_all(outros.c);
 	}
+	outros = outros.p;
 }
 
 export function transition_in(block, local?: 0 | 1) {
@@ -51,7 +53,7 @@ export function transition_out(block, local: 0 | 1, detach: 0 | 1, callback) {
 		if (outroing.has(block)) return;
 		outroing.add(block);
 
-		outros.callbacks.push(() => {
+		outros.c.push(() => {
 			outroing.delete(block);
 			if (callback) {
 				if (detach) block.d(1);
@@ -153,7 +155,7 @@ export function create_out_transition(node: Element & ElementCSSInlineStyle, fn:
 
 	const group = outros;
 
-	group.remaining += 1;
+	group.r += 1;
 
 	function go() {
 		const {
@@ -178,10 +180,10 @@ export function create_out_transition(node: Element & ElementCSSInlineStyle, fn:
 
 					dispatch(node, false, 'end');
 
-					if (!--group.remaining) {
+					if (!--group.r) {
 						// this will result in `end()` being called,
 						// so we don't need to clean up here
-						run_all(group.callbacks);
+						run_all(group.c);
 					}
 
 					return false;
@@ -266,7 +268,7 @@ export function create_bidirectional_transition(node: Element & ElementCSSInline
 		if (!b) {
 			// @ts-ignore todo: improve typings
 			program.group = outros;
-			outros.remaining += 1;
+			outros.r += 1;
 		}
 
 		if (running_program) {
@@ -309,7 +311,7 @@ export function create_bidirectional_transition(node: Element & ElementCSSInline
 								clear_animation();
 							} else {
 								// outro — needs to be coordinated
-								if (!--running_program.group.remaining) run_all(running_program.group.callbacks);
+								if (!--running_program.group.r) run_all(running_program.group.c);
 							}
 						}
 


Test patch:
diff --git a/test/runtime/samples/each-block-keyed-nested/Child.svelte b/test/runtime/samples/each-block-keyed-nested/Child.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-keyed-nested/Child.svelte
@@ -0,0 +1,5 @@
+<script>
+	export let id;
+</script>
+
+{id}
\ No newline at end of file
diff --git a/test/runtime/samples/each-block-keyed-nested/_config.js b/test/runtime/samples/each-block-keyed-nested/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-keyed-nested/_config.js
@@ -0,0 +1,16 @@
+export default {
+	html: `
+		1
+	`,
+
+	test({ assert, component, target }) {
+		component.desks = [
+			{
+				id: 1,
+				teams: []
+			}
+		];
+
+		assert.htmlEqual(target.innerHTML, '');
+	}
+};
diff --git a/test/runtime/samples/each-block-keyed-nested/main.svelte b/test/runtime/samples/each-block-keyed-nested/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-keyed-nested/main.svelte
@@ -0,0 +1,16 @@
+<script>
+	import Child from './Child.svelte';
+
+	export let desks = [
+		{
+			id: 1,
+			teams: [{ id: 1 }]
+		}
+	];
+</script>
+
+{#each desks as desk (desk.id)}
+	{#each desk.teams as team (team.id)}
+		<Child id={team.id} />
+	{/each}
+{/each}
\ No newline at end of file
diff --git a/test/runtime/samples/head-detached-in-dynamic-component/A.svelte b/test/runtime/samples/head-detached-in-dynamic-component/A.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/head-detached-in-dynamic-component/A.svelte
@@ -0,0 +1,5 @@
+<svelte:head>
+	<meta name="description" content="A"/>
+</svelte:head>
+
+A
\ No newline at end of file
diff --git a/test/runtime/samples/head-detached-in-dynamic-component/B.svelte b/test/runtime/samples/head-detached-in-dynamic-component/B.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/head-detached-in-dynamic-component/B.svelte
@@ -0,0 +1,5 @@
+<svelte:head>
+	<meta name="description" content="B"/>
+</svelte:head>
+
+B
\ No newline at end of file
diff --git a/test/runtime/samples/head-detached-in-dynamic-component/_config.js b/test/runtime/samples/head-detached-in-dynamic-component/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/head-detached-in-dynamic-component/_config.js
@@ -0,0 +1,15 @@
+export default {
+	html: `
+		A
+	`,
+
+	test({ assert, component, window }) {
+		component.x = false;
+
+		const meta = window.document.querySelectorAll('meta');
+
+		assert.equal(meta.length, 1);
+		assert.equal(meta[0].name, 'description');
+		assert.equal(meta[0].content, 'B');
+	}
+};
diff --git a/test/runtime/samples/head-detached-in-dynamic-component/main.svelte b/test/runtime/samples/head-detached-in-dynamic-component/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/head-detached-in-dynamic-component/main.svelte
@@ -0,0 +1,8 @@
+<script>
+	import A from './A.svelte';
+	import B from './B.svelte';
+
+	export let x = true;
+</script>
+
+<svelte:component this="{x ? A : B}"/>
\ No newline at end of file