Skip to content

Commit 6f855e6

Browse files
authored
fix: ensure reactivity system remains consistent with removals (#13087)
* fix: ensure reactivity system remains consistent with removals * more fixes * add test
1 parent 2d03dc5 commit 6f855e6

File tree

9 files changed

+105
-32
lines changed

9 files changed

+105
-32
lines changed

.changeset/chilly-waves-count.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: ensure reactivity system remains consistent with removals

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

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { Derived } from '#client' */
1+
/** @import { Derived, Effect } from '#client' */
22
import { DEV } from 'esm-env';
33
import { CLEAN, DERIVED, DESTROYED, DIRTY, MAYBE_DIRTY, UNOWNED } from '../constants.js';
44
import {
@@ -8,11 +8,11 @@ import {
88
set_signal_status,
99
current_skip_reaction,
1010
update_reaction,
11-
destroy_effect_children,
1211
increment_version
1312
} from '../runtime.js';
1413
import { equals, safe_equals } from './equality.js';
1514
import * as e from '../errors.js';
15+
import { destroy_effect } from './effects.js';
1616

1717
/**
1818
* @template V
@@ -26,25 +26,19 @@ export function derived(fn) {
2626

2727
/** @type {Derived<V>} */
2828
const signal = {
29+
children: null,
2930
deps: null,
30-
deriveds: null,
3131
equals,
3232
f: flags,
33-
first: null,
3433
fn,
35-
last: null,
3634
reactions: null,
3735
v: /** @type {V} */ (null),
3836
version: 0
3937
};
4038

4139
if (current_reaction !== null && (current_reaction.f & DERIVED) !== 0) {
42-
var current_derived = /** @type {Derived} */ (current_reaction);
43-
if (current_derived.deriveds === null) {
44-
current_derived.deriveds = [signal];
45-
} else {
46-
current_derived.deriveds.push(signal);
47-
}
40+
var derived = /** @type {Derived} */ (current_reaction);
41+
(derived.children ??= []).push(signal);
4842
}
4943

5044
return signal;
@@ -67,14 +61,18 @@ export function derived_safe_equal(fn) {
6761
* @returns {void}
6862
*/
6963
function destroy_derived_children(derived) {
70-
destroy_effect_children(derived);
71-
var deriveds = derived.deriveds;
64+
var children = derived.children;
7265

73-
if (deriveds !== null) {
74-
derived.deriveds = null;
66+
if (children !== null) {
67+
derived.children = null;
7568

76-
for (var i = 0; i < deriveds.length; i += 1) {
77-
destroy_derived(deriveds[i]);
69+
for (var i = 0; i < children.length; i += 1) {
70+
var child = children[i];
71+
if ((child.f & DERIVED) !== 0) {
72+
destroy_derived(/** @type {Derived} */ (child));
73+
} else {
74+
destroy_effect(/** @type {Effect} */ (child));
75+
}
7876
}
7977
}
8078
}
@@ -135,8 +133,7 @@ function destroy_derived(signal) {
135133

136134
// TODO we need to ensure we remove the derived from any parent derives
137135

138-
signal.first =
139-
signal.last =
136+
signal.children =
140137
signal.deps =
141138
signal.reactions =
142139
// @ts-expect-error `signal.fn` cannot be `null` while the signal is alive

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { ComponentContext, ComponentContextLegacy, Effect, Reaction, TemplateNode, TransitionManager } from '#client' */
1+
/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, Reaction, TemplateNode, TransitionManager } from '#client' */
22
import {
33
check_dirtiness,
44
current_component_context,
@@ -61,7 +61,7 @@ export function validate_effect(rune) {
6161

6262
/**
6363
* @param {Effect} effect
64-
* @param {Reaction} parent_effect
64+
* @param {Effect} parent_effect
6565
*/
6666
function push_effect(effect, parent_effect) {
6767
var parent_last = parent_effect.last;
@@ -147,7 +147,8 @@ function create_effect(type, fn, sync, push = true) {
147147

148148
// if we're in a derived, add the effect there too
149149
if (current_reaction !== null && (current_reaction.f & DERIVED) !== 0) {
150-
push_effect(effect, current_reaction);
150+
var derived = /** @type {Derived} */ (current_reaction);
151+
(derived.children ??= []).push(effect);
151152
}
152153
}
153154

@@ -396,7 +397,7 @@ export function destroy_effect(effect, remove_dom = true) {
396397
var parent = effect.parent;
397398

398399
// If the parent doesn't have any children, then skip this work altogether
399-
if (parent !== null && (effect.f & BRANCH_EFFECT) !== 0 && parent.first !== null) {
400+
if (parent !== null && parent.first !== null) {
400401
unlink_effect(effect);
401402
}
402403

packages/svelte/src/internal/client/reactivity/types.d.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,13 @@ export interface Reaction extends Signal {
2121
fn: null | Function;
2222
/** Signals that this signal reads from */
2323
deps: null | Value[];
24-
/** First child effect created inside this signal */
25-
first: null | Effect;
26-
/** Last child effect created inside this signal */
27-
last: null | Effect;
2824
}
2925

3026
export interface Derived<V = unknown> extends Value<V>, Reaction {
3127
/** The derived function */
3228
fn: () => V;
33-
/** Deriveds created inside this signal */
34-
deriveds: null | Derived[];
29+
/** Reactions created inside this signal */
30+
children: null | Reaction[];
3531
}
3632

3733
export interface Effect extends Reaction {
@@ -56,6 +52,10 @@ export interface Effect extends Reaction {
5652
prev: null | Effect;
5753
/** Next sibling child effect created inside the parent signal */
5854
next: null | Effect;
55+
/** First child effect created inside this signal */
56+
first: null | Effect;
57+
/** Last child effect created inside this signal */
58+
last: null | Effect;
5959
/** Dev only */
6060
component_function?: any;
6161
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ export function remove_reactions(signal, start_index) {
392392
}
393393

394394
/**
395-
* @param {Reaction} signal
395+
* @param {Effect} signal
396396
* @param {boolean} remove_dom
397397
* @returns {void}
398398
*/
@@ -601,9 +601,9 @@ function process_effects(effect, collected_effects) {
601601
if ((flags & RENDER_EFFECT) !== 0) {
602602
if (!is_branch && check_dirtiness(current_effect)) {
603603
update_effect(current_effect);
604-
// Child might have been mutated since running the effect
605-
child = current_effect.first;
606604
}
605+
// Child might have been mutated since running the effect or checking dirtiness
606+
child = current_effect.first;
607607

608608
if (child !== null) {
609609
current_effect = child;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
test({ assert, target }) {
6+
const button = target.querySelector('button');
7+
8+
flushSync(() => {
9+
button?.click();
10+
button?.click();
11+
button?.click();
12+
});
13+
assert.htmlEqual(target.innerHTML, `<div>3</div><div>3, 6</div><button>increment</button>`);
14+
}
15+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script>
2+
import { fromStore } from 'svelte/store';
3+
import { writable } from 'svelte/store';
4+
5+
const store = writable(0);
6+
7+
const value = fromStore(store);
8+
</script>
9+
10+
<div>{$store}</div>
11+
12+
{#if true}
13+
{@const doubled = value.current * 2}
14+
<div>{value.current}, {doubled}</div>
15+
{/if}
16+
17+
<button onclick={() => $store++}>increment</button>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
test({ assert, target }) {
6+
const button = target.querySelector('button');
7+
8+
flushSync(() => {
9+
button?.click();
10+
button?.click();
11+
button?.click();
12+
});
13+
assert.htmlEqual(target.innerHTML, `\n3\n<button>Increment</button><br>`);
14+
}
15+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script>
2+
import { writable, fromStore } from 'svelte/store';
3+
const store = writable(0)
4+
const state_from_store= fromStore(store)
5+
6+
const derived_value= $derived.by(() => {
7+
if (state_from_store.current > 10) {
8+
return state_from_store.current
9+
}
10+
else{
11+
return 10
12+
}
13+
})
14+
15+
function increment() {
16+
$store += 1;
17+
}
18+
</script>
19+
20+
{state_from_store.current}
21+
<button onclick={increment}>Increment</button><br>
22+
23+
{#if derived_value > 10 }Exceeded 10!{/if}

0 commit comments

Comments
 (0)