Repository: sveltejs/svelte (JavaScript)
Date: 2021-11-12 06:00:32+00:00
Task type: Bug Fix

Issue:
Svelte bind:this does not give proper working inside onMount body
### Describe the bug

After importing Svelte component manually, there are issues retrieving the HTML element with the 'bind:this' binding. After using it inside the onMount function body, the page should be fully loaded and the value 'should' be available. After using a setTimeout block, the issue seems to be resolved.

Issue found after using svelte extension; https://github.com/ANovokmet/svelte-gantt



Seems broken from version=3.18.2

### Reproduction

https://svelte.dev/repl/43c5c3945d354cfc8fffeb80f4099255?version=3.44.1

![image](https://user-images.githubusercontent.com/1366992/141366699-621e6139-082d-4c47-9cd9-3ba0e664c493.png)


### Logs

_No response_

### System Info

```shell
https://svelte.dev/repl 

version=3.44.1

'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36'
```


### Severity

annoyance


Issue discussion:
The [`bind:this` docs](https://svelte.dev/docs#bind_element) certainly do suggest that it should be available by the time `onMount()` runs, as that's precisely the scenario used in the docs example. Good repro REPL, thanks. It let me confirm that version 3.18.1 is the last version that worked, and that version 3.18.2 does not work. Also, there is no difference in the compiled output of your REPL between 3.18.1 and 3.18.2, so it's probably a difference in the runtime that caused this bug.
Interesting. If I change your REPL reproduction to mount the component like `<Window />` instead of using `new Window({ target: whatever })`, then the bug does not occur. So it's got something to do with the timing of component creation with `new` vs when components are created via the "normal" process.
The cause appears to have been https://github.com/sveltejs/svelte/pull/4356, first released in version 3.18.2. I made a local reproduction, and built a custom Svelte package using a local checkout of the Svelte repro. I made only one change to my custo Svelte package, reverting the changes to `flush()` made in #4356, and that fixed the bug.

We'll probably need to add a regression test that exercises this scenario: creating a component via `onMount(() = { new Component({target: whatever}); }` and having that component do `bind:this`. That was a scenario that #4356 broke, but no unit tests exercised that scenario at the time so the breakage wasn't noticed.
It's related to https://github.com/sveltejs/svelte/issues/6760

Solution patch:
diff --git a/src/runtime/internal/scheduler.ts b/src/runtime/internal/scheduler.ts
--- a/src/runtime/internal/scheduler.ts
+++ b/src/runtime/internal/scheduler.ts
@@ -1,5 +1,5 @@
 import { run_all } from './utils';
-import { set_current_component } from './lifecycle';
+import { current_component, set_current_component } from './lifecycle';
 
 export const dirty_components = [];
 export const intros = { enabled: false };
@@ -31,23 +31,42 @@ export function add_flush_callback(fn) {
 	flush_callbacks.push(fn);
 }
 
-let flushing = false;
+// flush() calls callbacks in this order:
+// 1. All beforeUpdate callbacks, in order: parents before children
+// 2. All bind:this callbacks, in reverse order: children before parents.
+// 3. All afterUpdate callbacks, in order: parents before children. EXCEPT
+//    for afterUpdates called during the initial onMount, which are called in
+//    reverse order: children before parents.
+// Since callbacks might update component values, which could trigger another
+// call to flush(), the following steps guard against this:
+// 1. During beforeUpdate, any updated components will be added to the
+//    dirty_components array and will cause a reentrant call to flush(). Because
+//    the flush index is kept outside the function, the reentrant call will pick
+//    up where the earlier call left off and go through all dirty components. The
+//    current_component value is saved and restored so that the reentrant call will
+//    not interfere with the "parent" flush() call.
+// 2. bind:this callbacks cannot trigger new flush() calls.
+// 3. During afterUpdate, any updated components will NOT have their afterUpdate
+//    callback called a second time; the seen_callbacks set, outside the flush()
+//    function, guarantees this behavior.
 const seen_callbacks = new Set();
+let flushidx = 0;  // Do *not* move this inside the flush() function
 export function flush() {
-	if (flushing) return;
-	flushing = true;
+	const saved_component = current_component;
 
 	do {
 		// first, call beforeUpdate functions
 		// and update components
-		for (let i = 0; i < dirty_components.length; i += 1) {
-			const component = dirty_components[i];
+		while (flushidx < dirty_components.length) {
+			const component = dirty_components[flushidx];
+			flushidx++;
 			set_current_component(component);
 			update(component.$$);
 		}
 		set_current_component(null);
 
 		dirty_components.length = 0;
+		flushidx = 0;
 
 		while (binding_callbacks.length) binding_callbacks.pop()();
 
@@ -73,8 +92,8 @@ export function flush() {
 	}
 
 	update_scheduled = false;
-	flushing = false;
 	seen_callbacks.clear();
+	set_current_component(saved_component);
 }
 
 function update($$) {


Test patch:
diff --git a/test/runtime/samples/component-binding-onMount/Mount.svelte b/test/runtime/samples/component-binding-onMount/Mount.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-onMount/Mount.svelte
@@ -0,0 +1,15 @@
+<script>
+	import { onMount } from 'svelte';
+
+	let element;
+	let bound = false;
+	onMount(() => {
+		if (element) bound = true;
+	});
+
+</script>
+
+<div bind:this={element}></div>
+<p>
+	Bound? {bound}
+</p>
diff --git a/test/runtime/samples/component-binding-onMount/_config.js b/test/runtime/samples/component-binding-onMount/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-onMount/_config.js
@@ -0,0 +1,11 @@
+export default {
+	async test({ assert, target }) {
+		assert.htmlEqual(target.innerHTML, `
+		<div id="target"><div></div>
+			<p>
+				Bound? true
+			</p>
+		</div>
+	`);
+	}
+};
diff --git a/test/runtime/samples/component-binding-onMount/main.svelte b/test/runtime/samples/component-binding-onMount/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-onMount/main.svelte
@@ -0,0 +1,13 @@
+<script>
+	import Mount from './Mount.svelte';
+	import { onMount } from 'svelte';
+
+	onMount(() => {
+		const component = new Mount({
+			target: document.querySelector('#target'),
+			props: {},
+		});
+	});
+</script>
+
+<div id="target" />