Skip to content

Commit 34748ba

Browse files
authored
chore: verify that $effect.root(...) does not re-run (#11020)
1 parent 0a16292 commit 34748ba

File tree

7 files changed

+75
-30
lines changed

7 files changed

+75
-30
lines changed

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export function push_effect(effect, parent_effect) {
5353
*/
5454
function create_effect(type, fn, sync) {
5555
var is_root = (type & ROOT_EFFECT) !== 0;
56+
5657
/** @type {import('#client').Effect} */
5758
var effect = {
5859
ctx: current_component_context,
@@ -150,9 +151,7 @@ export function user_pre_effect(fn) {
150151
* @returns {() => void}
151152
*/
152153
export function effect_root(fn) {
153-
// TODO is `untrack` correct here? Should `fn` re-run if its dependencies change?
154-
// Should it even be modelled as an effect?
155-
const effect = create_effect(ROOT_EFFECT, () => untrack(fn), true);
154+
const effect = create_effect(ROOT_EFFECT, fn, true);
156155
return () => {
157156
destroy_effect(effect);
158157
};

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

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -245,27 +245,25 @@ function _mount(
245245

246246
const unmount = effect_root(() => {
247247
branch(() => {
248-
untrack(() => {
249-
if (context) {
250-
push({});
251-
var ctx = /** @type {import('#client').ComponentContext} */ (current_component_context);
252-
ctx.c = context;
253-
}
254-
255-
if (events) {
256-
// We can't spread the object or else we'd lose the state proxy stuff, if it is one
257-
/** @type {any} */ (props).$$events = events;
258-
}
259-
260-
should_intro = intro;
261-
// @ts-expect-error the public typings are not what the actual function looks like
262-
component = Component(anchor, props) || {};
263-
should_intro = true;
264-
265-
if (context) {
266-
pop();
267-
}
268-
});
248+
if (context) {
249+
push({});
250+
var ctx = /** @type {import('#client').ComponentContext} */ (current_component_context);
251+
ctx.c = context;
252+
}
253+
254+
if (events) {
255+
// We can't spread the object or else we'd lose the state proxy stuff, if it is one
256+
/** @type {any} */ (props).$$events = events;
257+
}
258+
259+
should_intro = intro;
260+
// @ts-expect-error the public typings are not what the actual function looks like
261+
component = Component(anchor, props) || {};
262+
should_intro = true;
263+
264+
if (context) {
265+
pop();
266+
}
269267
});
270268

271269
return () => {

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import {
2121
INERT,
2222
BRANCH_EFFECT,
2323
STATE_SYMBOL,
24-
BLOCK_EFFECT
24+
BLOCK_EFFECT,
25+
ROOT_EFFECT
2526
} from './constants.js';
2627
import { flush_tasks } from './dom/task.js';
2728
import { add_owner } from './dev/ownership.js';
@@ -692,7 +693,7 @@ export function get(signal) {
692693
// Register the dependency on the current reaction signal.
693694
if (
694695
current_reaction !== null &&
695-
(current_reaction.f & BRANCH_EFFECT) === 0 &&
696+
(current_reaction.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 &&
696697
!current_untracking
697698
) {
698699
const unowned = (current_reaction.f & UNOWNED) !== 0;
@@ -741,6 +742,7 @@ export function get(signal) {
741742
update_derived(/** @type {import('./types.js').Derived} **/ (signal), false);
742743
}
743744
}
745+
744746
return signal.v;
745747
}
746748

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
import { log } from './log.js';
4+
5+
export default test({
6+
before_test() {
7+
log.length = 0;
8+
},
9+
10+
async test({ assert, target }) {
11+
const [b1, b2] = target.querySelectorAll('button');
12+
13+
flushSync(() => {
14+
b1.click();
15+
});
16+
17+
assert.deepEqual(log, [0]);
18+
19+
flushSync(() => {
20+
b2.click();
21+
});
22+
23+
assert.deepEqual(log, [0, 'cleanup']);
24+
25+
flushSync(() => {
26+
b1.click();
27+
});
28+
29+
assert.deepEqual(log, [0, 'cleanup']);
30+
}
31+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/** @type {any[]} */
2+
export const log = [];
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
import { log } from './log.js';
3+
4+
let x = $state(0);
5+
6+
const cleanup = $effect.root(() => {
7+
log.push(x);
8+
return () => log.push('cleanup');
9+
});
10+
</script>
11+
12+
<button onclick={() => x++}>{x}</button>
13+
<button onclick={cleanup}>cleanup</button>

packages/svelte/tests/runtime-runes/samples/effect-root/main.svelte

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
1212
const nested_cleanup = $effect.root(() => {
1313
return () => {
14-
log.push('cleanup 2') ;
14+
log.push('cleanup 2');
1515
}
1616
});
1717
@@ -22,6 +22,6 @@
2222
});
2323
</script>
2424

25-
<button on:click={() => x++}>{x}</button>
26-
<button on:click={() => y++}>{y}</button>
27-
<button on:click={() => cleanup()}>cleanup</button>
25+
<button onclick={() => x++}>{x}</button>
26+
<button onclick={() => y++}>{y}</button>
27+
<button onclick={cleanup}>cleanup</button>

0 commit comments

Comments
 (0)