Skip to content

Commit 7dbb5b4

Browse files
hmndteemingcRich-Harris
authored
fix: derived reactivity and perf regressions (#17362)
* add test sample * add test for sveltejs/kit#15059 * fix: reconnect deriveds inside branch effects * add changeset * fix: derived with no deps always set as MAYBE_DIRTY fixes #17342 * add test for #17342 * additional changeset * refactor: extract setting derived status to helper, apply to sources.js * add test case for #17352 * fix: reconnect child deriveds when evaluating connected parent derived fixes #17352 * fix import order causing Cannot read properties of undefined on dev load * remove duplicate iteration over deps * minor style tweaks * oops, fix merge * use update_derived_status, so that we never set a dep-less derived MAYBE_DIRTY * tweak * reaction.deps cannot be null for a MAYBE_DIRTY derived * make it such that reactions without deps are never MAYBE_DIRTY * since we no longer need to check reaction.deps === null, we can revert this bit * more explicit check * tidy up * more * gah whoops * move import * simplify test * make dep-less derived behaviour more explicit, move it above is_destroying_effect handling * remove test - this is adequately covered by #17445 * replace tricky unit test with component-based test * remove incorrect test * remove the BRANCH_EFFECT stuff * tidy up * DRY * tweak, add explanatory comment * tweak * explanatory comment * remove changeset * update changeset --------- Co-authored-by: Tee Ming <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent ae224be commit 7dbb5b4

File tree

4 files changed

+101
-7
lines changed

4 files changed

+101
-7
lines changed

.changeset/beige-sloths-cry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: reconnect clean deriveds when they are read in a reactive context

packages/svelte/src/internal/client/runtime.js

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -620,15 +620,27 @@ export function get(signal) {
620620
return value;
621621
}
622622

623-
// TODO this should probably just be `!batch_values?.has(derived)` — the second bit
624-
// should be taken care of by clearing `batch_values` in `mark_reactions`?
625-
if (!batch_values?.has(derived) || (current_batch?.is_fork && !effect_tracking())) {
626-
if (is_dirty(derived)) {
627-
update_derived(derived);
623+
// connect disconnected deriveds if we are reading them inside an effect,
624+
// or inside another derived that is already connected
625+
var should_connect =
626+
(derived.f & CONNECTED) === 0 &&
627+
!untracking &&
628+
active_reaction !== null &&
629+
(is_updating_effect || (active_reaction.f & CONNECTED) !== 0);
630+
631+
var is_new = derived.deps === null;
632+
633+
if (is_dirty(derived)) {
634+
if (should_connect) {
635+
// set the flag before `update_derived`, so that the derived
636+
// is added as a reaction to its dependencies
637+
derived.f |= CONNECTED;
628638
}
639+
640+
update_derived(derived);
629641
}
630642

631-
if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) {
643+
if (should_connect && !is_new) {
632644
reconnect(derived);
633645
}
634646
}
@@ -652,7 +664,7 @@ export function get(signal) {
652664
function reconnect(derived) {
653665
if (derived.deps === null) return;
654666

655-
derived.f ^= CONNECTED;
667+
derived.f |= CONNECTED;
656668

657669
for (const dep of derived.deps) {
658670
(dep.reactions ??= []).push(derived);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
html: `
6+
<button>+1</button>
7+
<button>add number</button>
8+
<p>1, 2, 3</p>
9+
`,
10+
11+
test({ assert, target }) {
12+
const [button1, button2] = target.querySelectorAll('button');
13+
14+
button1.click();
15+
flushSync();
16+
assert.htmlEqual(
17+
target.innerHTML,
18+
`
19+
<button>+1</button>
20+
<button>add number</button>
21+
<p>2, 4, 6</p>
22+
`
23+
);
24+
25+
button2.click();
26+
flushSync();
27+
assert.htmlEqual(
28+
target.innerHTML,
29+
`
30+
<button>+1</button>
31+
<button>add number</button>
32+
<p>2, 4, 6, 8</p>
33+
`
34+
);
35+
36+
button1.click();
37+
flushSync();
38+
assert.htmlEqual(
39+
target.innerHTML,
40+
`
41+
<button>+1</button>
42+
<button>add number</button>
43+
<p>3, 6, 9, 12</p>
44+
`
45+
);
46+
}
47+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<script lang="ts">
2+
class Item {
3+
product: number;
4+
5+
constructor(n: number) {
6+
this.product = $derived(multiplier * n);
7+
}
8+
}
9+
10+
let numbers = $state([1, 2, 3]);
11+
let multiplier = $state(1);
12+
13+
let items = $derived(numbers.map((n) => new Item(n)))
14+
let products = $derived(items.map(item => item.product));
15+
</script>
16+
17+
<button onclick={() => {
18+
multiplier += 1;
19+
}}>+1</button>
20+
21+
<button onclick={() => {
22+
numbers.push(numbers.length + 1);
23+
24+
// this is load-bearing — by reading it outside a reaction, we recompute
25+
// `products`, removing it as a reaction from `Item.product` dependencies,
26+
// but we don't add it as a reaction to the new `Item.product` dependencies
27+
products;
28+
}}>add number</button>
29+
30+
<p>{products.join(', ')}</p>

0 commit comments

Comments
 (0)