diff --git a/.changeset/soft-clocks-remember.md b/.changeset/soft-clocks-remember.md
new file mode 100644
index 000000000000..69e8aca06e3c
--- /dev/null
+++ b/.changeset/soft-clocks-remember.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: improve consistency issues around binding invalidation
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
index fb43ec654b11..e784d5407b51 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
@@ -245,7 +245,7 @@ function setup_select_synchronization(value_binding, context) {
context.state.init.push(
b.stmt(
b.call(
- '$.pre_effect',
+ '$.invalidate_effect',
b.thunk(
b.block([
b.stmt(
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 101a189b1082..0840bbe59283 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -1298,6 +1298,17 @@ export function pre_effect(init) {
);
}
+/**
+ * This effect is used to ensure binding are kept in sync. We use a pre effect to ensure we run before the
+ * bindings which are in later effects. However, we don't use a pre_effect directly as we don't want to flush anything.
+ *
+ * @param {() => void | (() => void)} init
+ * @returns {import('./types.js').EffectSignal}
+ */
+export function invalidate_effect(init) {
+ return internal_create_effect(PRE_EFFECT, init, true, current_block, true);
+}
+
/**
* @param {() => void | (() => void)} init
* @returns {import('./types.js').EffectSignal}
diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js
index 4d3205b8242d..a94a0de180cc 100644
--- a/packages/svelte/src/internal/index.js
+++ b/packages/svelte/src/internal/index.js
@@ -12,6 +12,7 @@ export {
user_effect,
render_effect,
pre_effect,
+ invalidate_effect,
flushSync,
bubble_event,
safe_equal,
diff --git a/packages/svelte/tests/runtime-runes/samples/invalidate-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/invalidate-effect/_config.js
new file mode 100644
index 000000000000..b4ba660ad471
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/invalidate-effect/_config.js
@@ -0,0 +1,16 @@
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ assert.htmlEqual(target.innerHTML, 'a\nb\n
+ let entries = $state([{selected: 'a'}])
+
+
+{#each entries as entry}
+ {entry.selected}
+{/each}
+
+