Repository: sveltejs/svelte (JavaScript)
Date: 2019-11-10 07:58:20+00:00
Task type: Bug Fix

Issue:
Prop binding goes out of sync when reactively updated
**Describe the bug**
If a bound prop is immediately updated as the result of state propagating upward, that update does not propagate downward.

**To Reproduce**
[REPL](https://svelte.dev/repl/74f3946d44ac43038afb24069bf64af5?version=3.6.1)
Clicking the `RefactoredButton` causes the internal state of `RefactoredButton` and `value` to go out of sync.

**Expected behavior**
The update to `value` should propagate downward to `RefactoredButton`. The behavior of clicking the inline button and the refactored component should be the same.

**Severity**
The workaround for this is relatively simple (just wrap the update in a `requestAnimationFrame`), but it's still frustrating and unexpected behavior.




Issue discussion:

Solution patch:
diff --git a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts
--- a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts
+++ b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts
@@ -300,7 +300,9 @@ export default class InlineComponentWrapper extends Wrapper {
 
 			updates.push(b`
 				if (!${updating} && ${changed(Array.from(binding.expression.dependencies))}) {
+					${updating} = true;
 					${name_changes}.${binding.name} = ${snippet};
+					@add_flush_callback(() => ${updating} = false);
 				}
 			`);
 
@@ -337,8 +339,6 @@ export default class InlineComponentWrapper extends Wrapper {
 				block.chunks.init.push(b`
 					function ${id}(${value}) {
 						#ctx.${id}.call(null, ${value}, #ctx);
-						${updating} = true;
-						@add_flush_callback(() => ${updating} = false);
 					}
 				`);
 
@@ -347,8 +347,6 @@ export default class InlineComponentWrapper extends Wrapper {
 				block.chunks.init.push(b`
 					function ${id}(${value}) {
 						#ctx.${id}.call(null, ${value});
-						${updating} = true;
-						@add_flush_callback(() => ${updating} = false);
 					}
 				`);
 			}


Test patch:
diff --git a/test/runtime/samples/component-binding-reactive-statement/Button.svelte b/test/runtime/samples/component-binding-reactive-statement/Button.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-reactive-statement/Button.svelte
@@ -0,0 +1,11 @@
+<script>
+	export let count
+	
+	function handleClick() {
+		count += 1;
+	}
+</script>
+
+<button on:click={handleClick}>
+	button {count}
+</button>
\ No newline at end of file
diff --git a/test/runtime/samples/component-binding-reactive-statement/_config.js b/test/runtime/samples/component-binding-reactive-statement/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-reactive-statement/_config.js
@@ -0,0 +1,38 @@
+export default {
+	html: `
+		<button>main 0</button>
+		<button>button 0</button>
+	`,
+
+	async test({ assert, component, target, window }) {
+		const event = new window.MouseEvent('click');
+
+		const buttons = target.querySelectorAll('button');
+
+		await buttons[0].dispatchEvent(event);
+		assert.htmlEqual(target.innerHTML, `
+			<button>main 1</button>
+			<button>button 1</button>
+		`);
+
+		await buttons[1].dispatchEvent(event);
+		assert.htmlEqual(target.innerHTML, `
+			<button>main 2</button>
+			<button>button 2</button>
+		`);
+
+		// reactive update, reset to 2
+		await buttons[0].dispatchEvent(event);
+		assert.htmlEqual(target.innerHTML, `
+			<button>main 2</button>
+			<button>button 2</button>
+		`);
+
+		// bound to main, reset to 2
+		await buttons[1].dispatchEvent(event);
+		assert.htmlEqual(target.innerHTML, `
+			<button>main 2</button>
+			<button>button 2</button>
+		`);
+	}
+};
diff --git a/test/runtime/samples/component-binding-reactive-statement/main.svelte b/test/runtime/samples/component-binding-reactive-statement/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-binding-reactive-statement/main.svelte
@@ -0,0 +1,19 @@
+<script>
+	import Button from './Button.svelte';
+	
+	let count = 0;
+
+	$: if (count > 2) {
+		count = 2;
+	}
+
+	function handleClick() {
+		count += 1;
+	}
+</script>
+
+<button on:click={handleClick}>
+	main {count}
+</button>
+
+<Button bind:count />