diff --git a/.changeset/proud-dots-swim.md b/.changeset/proud-dots-swim.md
new file mode 100644
index 0000000000..3f86f115ae
--- /dev/null
+++ b/.changeset/proud-dots-swim.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: invoke `$state.link` callback at the correct time
diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js
index d0c6441040..d6cc28c5c7 100644
--- a/packages/svelte/src/internal/client/reactivity/sources.js
+++ b/packages/svelte/src/internal/client/reactivity/sources.js
@@ -27,6 +27,7 @@ import {
} from '../constants.js';
import * as e from '../errors.js';
import { derived } from './deriveds.js';
+import { render_effect } from './effects.js';
let inspect_effects = new Set();
@@ -53,44 +54,23 @@ export function source(v) {
* @returns {(value?: V) => V}
*/
export function source_link(get_value, callback) {
- var was_local = false;
- var init = false;
- var local_source = source(/** @type {V} */ (undefined));
+ var s = source(/** @type {V} */ (undefined));
+ var ran = false;
- var linked_derived = derived(() => {
- var local_value = /** @type {V} */ (get(local_source));
- var linked_value = get_value();
+ callback ??= (value) => set(s, value);
- if (was_local) {
- was_local = false;
- return local_value;
- }
-
- return linked_value;
- });
-
- return function (/** @type {any} */ value) {
- if (arguments.length > 0) {
- was_local = true;
- set(local_source, value);
- get(linked_derived);
- return value;
- }
-
- var linked_value = get(linked_derived);
-
- if (init) {
- if (callback !== undefined) {
- untrack(() => callback(linked_value));
- return local_source.v;
- }
+ render_effect(() => {
+ if (ran) {
+ callback(get_value());
} else {
- init = true;
+ s.v = get_value();
}
+ });
- local_source.v = linked_value;
+ ran = true;
- return linked_value;
+ return function (/** @type {any} */ value) {
+ return arguments.length === 1 ? set(s, /** @type {V} */ (value)) : get(s);
};
}
diff --git a/packages/svelte/tests/runtime-runes/samples/state-link-callback/_config.js b/packages/svelte/tests/runtime-runes/samples/state-link-callback/_config.js
new file mode 100644
index 0000000000..978fdb14d6
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/state-link-callback/_config.js
@@ -0,0 +1,22 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ html: ``,
+
+ test({ assert, target, logs }) {
+ const [btn1, btn2] = target.querySelectorAll('button');
+
+ flushSync(() => btn1.click());
+ assert.deepEqual(logs, ['in callback 1']);
+ assert.htmlEqual(target.innerHTML, ``);
+
+ flushSync(() => btn2.click());
+ assert.deepEqual(logs, ['in callback 1']);
+ assert.htmlEqual(target.innerHTML, ``);
+
+ flushSync(() => btn1.click());
+ assert.deepEqual(logs, ['in callback 1', 'in callback 2']);
+ assert.htmlEqual(target.innerHTML, ``);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/state-link-callback/main.svelte b/packages/svelte/tests/runtime-runes/samples/state-link-callback/main.svelte
new file mode 100644
index 0000000000..8ef97c454f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/state-link-callback/main.svelte
@@ -0,0 +1,10 @@
+
+
+
+