Skip to content

Commit b3b698e

Browse files
committed
fix: $state.link bug fixes
1 parent 363a541 commit b3b698e

File tree

7 files changed

+118
-31
lines changed

7 files changed

+118
-31
lines changed

packages/svelte/src/internal/client/reactivity/sources.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import * as e from '../errors.js';
2929
import { derived } from './deriveds.js';
3030

3131
let inspect_effects = new Set();
32+
let allow_mutations = false;
3233

3334
/**
3435
* @template V
@@ -56,10 +57,11 @@ export function source_link(get_value, callback) {
5657
var was_local = false;
5758
var init = false;
5859
var local_source = source(/** @type {V} */ (undefined));
60+
var cached_linked_value = derived(get_value);
5961

6062
var linked_derived = derived(() => {
6163
var local_value = /** @type {V} */ (get(local_source));
62-
var linked_value = get_value();
64+
var linked_value = get(cached_linked_value);
6365

6466
if (was_local) {
6567
was_local = false;
@@ -69,6 +71,17 @@ export function source_link(get_value, callback) {
6971
return linked_value;
7072
});
7173

74+
/** @type {Derived<V> | undefined} */
75+
var callback_derived;
76+
77+
if (callback !== undefined) {
78+
callback_derived = derived(() => {
79+
var linked_value = get(cached_linked_value);
80+
untrack(() => callback(linked_value));
81+
return local_source.v;
82+
});
83+
}
84+
7285
return function (/** @type {any} */ value) {
7386
if (arguments.length > 0) {
7487
was_local = true;
@@ -78,10 +91,11 @@ export function source_link(get_value, callback) {
7891
}
7992

8093
var linked_value = get(linked_derived);
94+
get(local_source);
8195

8296
if (init) {
83-
if (callback !== undefined) {
84-
untrack(() => callback(linked_value));
97+
if (callback_derived !== undefined) {
98+
get(callback_derived);
8599
return local_source.v;
86100
}
87101
} else {
@@ -133,7 +147,12 @@ export function mutate(source, value) {
133147
* @returns {V}
134148
*/
135149
export function set(source, value) {
136-
if (current_reaction !== null && is_runes() && (current_reaction.f & DERIVED) !== 0) {
150+
if (
151+
!allow_mutations &&
152+
current_reaction !== null &&
153+
is_runes() &&
154+
(current_reaction.f & DERIVED) !== 0
155+
) {
137156
e.state_unsafe_mutation();
138157
}
139158

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
html: `<button>0</button><button>0</button><button>false</button>`,
6+
7+
test({ assert, target }) {
8+
const [btn1, btn2, btn3] = target.querySelectorAll('button');
9+
10+
flushSync(() => btn1.click());
11+
assert.htmlEqual(
12+
target.innerHTML,
13+
`<button>1</button><button>1</button><button>false</button>`
14+
);
15+
16+
flushSync(() => btn2.click());
17+
assert.htmlEqual(
18+
target.innerHTML,
19+
`<button>1</button><button>2</button><button>false</button>`
20+
);
21+
22+
flushSync(() => btn3.click());
23+
assert.htmlEqual(target.innerHTML, `<button>1</button><button>2</button><button>true</button>`);
24+
25+
flushSync(() => btn1.click());
26+
assert.htmlEqual(target.innerHTML, `<button>2</button><button>2</button><button>true</button>`);
27+
28+
flushSync(() => btn1.click());
29+
assert.htmlEqual(target.innerHTML, `<button>3</button><button>2</button><button>true</button>`);
30+
31+
flushSync(() => btn1.click());
32+
flushSync(() => btn3.click());
33+
assert.htmlEqual(
34+
target.innerHTML,
35+
`<button>4</button><button>2</button><button>false</button>`
36+
);
37+
flushSync(() => btn1.click());
38+
assert.htmlEqual(
39+
target.innerHTML,
40+
`<button>5</button><button>5</button><button>false</button>`
41+
);
42+
}
43+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script>
2+
let a = $state(0);
3+
let b = $state.link(a, (value) => {
4+
if (c) return;
5+
b = value;
6+
});
7+
let c = $state(false);
8+
</script>
9+
10+
<button onclick={() => a++}>{a}</button>
11+
<button onclick={() => b++}>{b}</button>
12+
<button onclick={() => c = !c}>{c}</button>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
mode: ['client'], // TODO: make this work in SSR too
5+
6+
test({ assert, logs }) {
7+
assert.deepEqual(logs, [0, 1, 2, 2, 2, 3]);
8+
}
9+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<script>
2+
let a = $state(0);
3+
let b = $state.link(a, (value) => {
4+
if (c) return;
5+
b = value;
6+
});
7+
let c = $state(false);
8+
9+
console.log(b);
10+
a++;
11+
console.log(b);
12+
b++;
13+
console.log(b);
14+
c = true;
15+
console.log(b);
16+
a++;
17+
console.log(b);
18+
b++;
19+
console.log(b);
20+
</script>
21+

packages/svelte/tests/runtime-runes/samples/state-link-callback/_config.js

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,21 @@ import { flushSync } from 'svelte';
22
import { test } from '../../test';
33

44
export default test({
5-
html: `<button>0</button><button>0</button><button>false</button>`,
5+
html: `<button>0</button><button>0</button>`,
66

7-
test({ assert, target }) {
8-
const [btn1, btn2, btn3] = target.querySelectorAll('button');
7+
test({ assert, target, logs }) {
8+
const [btn1, btn2] = target.querySelectorAll('button');
99

1010
flushSync(() => btn1.click());
11-
assert.htmlEqual(
12-
target.innerHTML,
13-
`<button>1</button><button>1</button><button>false</button>`
14-
);
11+
assert.deepEqual(logs, ['in callback 1']);
12+
assert.htmlEqual(target.innerHTML, `<button>1</button><button>1</button>`);
1513

1614
flushSync(() => btn2.click());
17-
assert.htmlEqual(
18-
target.innerHTML,
19-
`<button>1</button><button>2</button><button>false</button>`
20-
);
21-
22-
flushSync(() => btn3.click());
23-
assert.htmlEqual(target.innerHTML, `<button>1</button><button>2</button><button>true</button>`);
24-
25-
flushSync(() => btn1.click());
26-
assert.htmlEqual(target.innerHTML, `<button>2</button><button>2</button><button>true</button>`);
27-
28-
flushSync(() => btn1.click());
29-
assert.htmlEqual(target.innerHTML, `<button>3</button><button>2</button><button>true</button>`);
15+
assert.deepEqual(logs, ['in callback 1']);
16+
assert.htmlEqual(target.innerHTML, `<button>1</button><button>2</button>`);
3017

3118
flushSync(() => btn1.click());
32-
flushSync(() => btn3.click());
33-
assert.htmlEqual(
34-
target.innerHTML,
35-
`<button>4</button><button>4</button><button>false</button>`
36-
);
19+
assert.deepEqual(logs, ['in callback 1', 'in callback 2']);
20+
assert.htmlEqual(target.innerHTML, `<button>2</button><button>2</button>`);
3721
}
3822
});
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
<script>
22
let a = $state(0);
33
let b = $state.link(a, (value) => {
4-
if (c) return;
4+
console.log(`in callback ${value}`);
55
b = value;
66
});
77
let c = $state(false);
88
</script>
99

1010
<button onclick={() => a++}>{a}</button>
1111
<button onclick={() => b++}>{b}</button>
12-
<button onclick={() => c = !c}>{c}</button>

0 commit comments

Comments
 (0)