Repository: sveltejs/svelte (2025-08-19T23:51:15Z)

Issue:
Two way binding to a property of an object in an array update is delayed
### Describe the bug

If I use a complex two way binding to read and update a value on an object in an array that is inside a class _(see minimal reproduction code below)_ I get weird behaviour where the update doesn't happen until a second input. 

```html
<script>
	class MyStore {
		arrayOfComplexObjects = $state([{ value: "value" }])
		
		updateObject = (x) => {
			this.arrayOfComplexObjects = this.arrayOfComplexObjects.map(_ => ({ value:x }))
		}
	}
	
	const myStore = new MyStore();
</script>

{#each myStore.arrayOfComplexObjects as obj}
	<div>
		{obj.value}
	</div>
	<input bind:value={() => obj.value, (x) => myStore.updateObject(x)} />
{/each}

```

This issue does not occur on version 5.35.1.

### Reproduction

The issue is reproduced in a minimal reproduction here

https://svelte.dev/playground/ac953fd28f2941ef8a8f105461d061f3?version=5.38.1

### Logs

No relevant logs.

### System Info

```shell
System:
    OS: Windows 11 10.0.26100
    CPU: (12) x64 AMD Ryzen 5 PRO 8540U w/ Radeon 740M Graphics
    Memory: 3.54 GB / 14.67 GB
  Binaries:
    Node: 22.17.0 - ~\AppData\Local\Volta\tools\image\node\22.17.0\node.EXE 
    npm: 10.9.2 - ~\AppData\Local\Volta\tools\image\node\22.17.0\npm.CMD    
  Browsers:
    Edge: Chromium (139.0.3405.102)
  npmPackages:
    svelte: ^5.0.0 => 5.37.2
```

### Severity

blocking an upgrade


Discussion:
Possibly another case where our "write to input" binding logic introduces in the async PR fails.



Patch:
diff --git a/.changeset/rare-cups-fold.md b/.changeset/rare-cups-fold.md
new file mode 100644
index 000000000000..8cd5995651c2
--- /dev/null
+++ b/.changeset/rare-cups-fold.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: Introduced Promise.resolve to ensure that the 'set' operation completes before the 'get' operation Minimizing update delays.
diff --git a/.changeset/tasty-chicken-care.md b/.changeset/tasty-chicken-care.md
new file mode 100644
index 000000000000..ea579efe4c6e
--- /dev/null
+++ b/.changeset/tasty-chicken-care.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: wait until changes propagate before updating input selection state
diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js
index 67e6ff1dd2be..815acde7c53b 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js
@@ -6,7 +6,7 @@ import * as e from '../../../errors.js';
 import { is } from '../../../proxy.js';
 import { queue_micro_task } from '../../task.js';
 import { hydrating } from '../../hydration.js';
-import { untrack } from '../../../runtime.js';
+import { tick, untrack } from '../../../runtime.js';
 import { is_runes } from '../../../context.js';
 import { current_batch, previous_batch } from '../../../reactivity/batch.js';
 
@@ -17,11 +17,9 @@ import { current_batch, previous_batch } from '../../../reactivity/batch.js';
  * @returns {void}
  */
 export function bind_value(input, get, set = get) {
-	var runes = is_runes();
-
 	var batches = new WeakSet();
 
-	listen_to_event_and_reset_event(input, 'input', (is_reset) => {
+	listen_to_event_and_reset_event(input, 'input', async (is_reset) => {
 		if (DEV && input.type === 'checkbox') {
 			// TODO should this happen in prod too?
 			e.bind_invalid_checkbox_value();
@@ -36,9 +34,13 @@ export function bind_value(input, get, set = get) {
 			batches.add(current_batch);
 		}
 
-		// In runes mode, respect any validation in accessors (doesn't apply in legacy mode,
-		// because we use mutable state which ensures the render effect always runs)
-		if (runes && value !== (value = get())) {
+		// Because `{#each ...}` blocks work by updating sources inside the flush,
+		// we need to wait a tick before checking to see if we should forcibly
+		// update the input and reset the selection state
+		await tick();
+
+		// Respect any validation in accessors
+		if (value !== (value = get())) {
 			var start = input.selectionStart;
 			var end = input.selectionEnd;
 


Test patch:
diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/_config.js
new file mode 100644
index 000000000000..b6371ce11c96
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/_config.js
@@ -0,0 +1,28 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+	mode: ['client', 'hydrate'],
+
+	html: `<input><p>a</a>`,
+
+	async test({ assert, target }) {
+		const [input] = target.querySelectorAll('input');
+
+		input.focus();
+		input.value = 'ab';
+		input.dispatchEvent(new InputEvent('input', { bubbles: true }));
+		flushSync();
+
+		assert.htmlEqual(target.innerHTML, `<input><p>ab</a>`);
+		assert.equal(input.value, 'ab');
+
+		input.focus();
+		input.value = 'abc';
+		input.dispatchEvent(new InputEvent('input', { bubbles: true }));
+		flushSync();
+
+		assert.htmlEqual(target.innerHTML, `<input><p>abc</a>`);
+		assert.equal(input.value, 'abc');
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/main.svelte
new file mode 100644
index 000000000000..7925195ee159
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/binding-update-in-each/main.svelte
@@ -0,0 +1,8 @@
+<script>
+	let array = $state([{ value: 'a' }]);
+</script>
+
+{#each array as obj}
+	<input bind:value={() => obj.value, (value) => array = [{ value }]} />
+	<p>{obj.value}</p>
+{/each}
diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/_config.js
new file mode 100644
index 000000000000..0909dee7a8b9
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/_config.js
@@ -0,0 +1,30 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+	mode: ['client', 'hydrate'],
+
+	async test({ assert, target }) {
+		const [input] = target.querySelectorAll('input');
+
+		input.focus();
+		input.value = 'Ab';
+		input.dispatchEvent(new InputEvent('input', { bubbles: true }));
+
+		await tick();
+		await tick();
+
+		assert.equal(input.value, 'AB');
+		assert.htmlEqual(target.innerHTML, `<input /><p>AB</p>`);
+
+		input.focus();
+		input.value = 'ABc';
+		input.dispatchEvent(new InputEvent('input', { bubbles: true }));
+
+		await tick();
+		await tick();
+
+		assert.equal(input.value, 'ABC');
+		assert.htmlEqual(target.innerHTML, `<input /><p>ABC</p>`);
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/main.svelte
new file mode 100644
index 000000000000..b61bfe4e6714
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-3/main.svelte
@@ -0,0 +1,6 @@
+<script>
+	let text = $state('A');
+</script>
+
+<input bind:value={() => text, (v) => text = v.toUpperCase()} />
+<p>{text}</p>