Skip to content

Commit 8742823

Browse files
Rich-Harristrueadm
andauthored
fix: make $effect.active() true when updating deriveds (#11500)
* fix: make `$effect.active()` true when updating deriveds * WIP * this seems to work? * prevent effects being created in unowned deriveds * update test * fix issue --------- Co-authored-by: Dominic Gannaway <[email protected]>
1 parent 30caaef commit 8742823

File tree

8 files changed

+129
-10
lines changed

8 files changed

+129
-10
lines changed

.changeset/serious-goats-tap.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: make `$effect.active()` true when updating deriveds

packages/svelte/messages/client-errors/errors.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424

2525
> `%rune%` cannot be used inside an effect cleanup function
2626
27+
## effect_in_unowned_derived
28+
29+
> Effect cannot be created inside a `$derived` value that was not itself created inside an effect
30+
2731
## effect_orphan
2832

2933
> `%rune%` can only be used inside an effect (e.g. during component initialisation)

packages/svelte/src/internal/client/dev/inspect.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export let inspect_captured_signals = [];
2020
*/
2121
// eslint-disable-next-line no-console
2222
export function inspect(get_value, inspector = console.log) {
23-
validate_effect(current_effect, '$inspect');
23+
validate_effect('$inspect');
2424

2525
let initial = true;
2626

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,22 @@ export function effect_in_teardown(rune) {
111111
}
112112
}
113113

114+
/**
115+
* Effect cannot be created inside a `$derived` value that was not itself created inside an effect
116+
* @returns {never}
117+
*/
118+
export function effect_in_unowned_derived() {
119+
if (DEV) {
120+
const error = new Error(`${"effect_in_unowned_derived"}\n${"Effect cannot be created inside a `$derived` value that was not itself created inside an effect"}`);
121+
122+
error.name = 'Svelte error';
123+
throw error;
124+
} else {
125+
// TODO print a link to the documentation
126+
throw new Error("effect_in_unowned_derived");
127+
}
128+
}
129+
114130
/**
115131
* `%rune%` can only be used inside an effect (e.g. during component initialisation)
116132
* @param {string} rune

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export function update_derived(derived, force_schedule) {
121121
*/
122122
export function destroy_derived(signal) {
123123
destroy_derived_children(signal);
124+
destroy_effect_children(signal);
124125
remove_reactions(signal, 0);
125126
set_signal_status(signal, DESTROYED);
126127

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

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,20 @@ import {
2626
EFFECT_RAN,
2727
BLOCK_EFFECT,
2828
ROOT_EFFECT,
29-
EFFECT_TRANSPARENT
29+
EFFECT_TRANSPARENT,
30+
DERIVED,
31+
UNOWNED
3032
} from '../constants.js';
3133
import { set } from './sources.js';
3234
import { remove } from '../dom/reconciler.js';
3335
import * as e from '../errors.js';
3436
import { DEV } from 'esm-env';
3537

3638
/**
37-
* @param {import('#client').Effect | null} effect
3839
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
39-
* @returns {asserts effect}
4040
*/
41-
export function validate_effect(effect, rune) {
42-
if (effect === null) {
41+
export function validate_effect(rune) {
42+
if (current_effect === null && current_reaction === null) {
4343
e.effect_orphan(rune);
4444
}
4545

@@ -93,6 +93,18 @@ function create_effect(type, fn, sync) {
9393
}
9494

9595
if (current_reaction !== null && !is_root) {
96+
var flags = current_reaction.f;
97+
if ((flags & DERIVED) !== 0) {
98+
if ((flags & UNOWNED) !== 0) {
99+
e.effect_in_unowned_derived();
100+
}
101+
// If we are inside a derived, then we also need to attach the
102+
// effect to the parent effect too.
103+
if (current_effect !== null) {
104+
push_effect(effect, current_effect);
105+
}
106+
}
107+
96108
push_effect(effect, current_reaction);
97109
}
98110

@@ -118,20 +130,29 @@ function create_effect(type, fn, sync) {
118130
* @returns {boolean}
119131
*/
120132
export function effect_active() {
121-
return current_effect ? (current_effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 : false;
133+
if (current_reaction && (current_reaction.f & DERIVED) !== 0) {
134+
return (current_reaction.f & UNOWNED) === 0;
135+
}
136+
137+
if (current_effect) {
138+
return (current_effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0;
139+
}
140+
141+
return false;
122142
}
123143

124144
/**
125145
* Internal representation of `$effect(...)`
126146
* @param {() => void | (() => void)} fn
127147
*/
128148
export function user_effect(fn) {
129-
validate_effect(current_effect, '$effect');
149+
validate_effect('$effect');
130150

131151
// Non-nested `$effect(...)` in a component should be deferred
132152
// until the component is mounted
133153
const defer =
134-
current_effect.f & RENDER_EFFECT &&
154+
current_effect !== null &&
155+
(current_effect.f & RENDER_EFFECT) !== 0 &&
135156
// TODO do we actually need this? removing them changes nothing
136157
current_component_context !== null &&
137158
!current_component_context.m;
@@ -150,7 +171,7 @@ export function user_effect(fn) {
150171
* @returns {import('#client').Effect}
151172
*/
152173
export function user_pre_effect(fn) {
153-
validate_effect(current_effect, '$effect.pre');
174+
validate_effect('$effect.pre');
154175
return render_effect(fn);
155176
}
156177

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
html: `
6+
<button>toggle outer</button>
7+
<button>toggle inner</button>
8+
<button>reset</button>
9+
`,
10+
11+
test({ assert, target }) {
12+
const [outer, inner, reset] = target.querySelectorAll('button');
13+
14+
flushSync(() => outer?.click());
15+
flushSync(() => inner?.click());
16+
17+
assert.htmlEqual(
18+
target.innerHTML,
19+
`
20+
<button>toggle outer</button>
21+
<button>toggle inner</button>
22+
<button>reset</button>
23+
<p>v is true</p>
24+
`
25+
);
26+
27+
flushSync(() => reset?.click());
28+
flushSync(() => inner?.click());
29+
flushSync(() => outer?.click());
30+
31+
assert.htmlEqual(
32+
target.innerHTML,
33+
`
34+
<button>toggle outer</button>
35+
<button>toggle inner</button>
36+
<button>reset</button>
37+
<p>v is true</p>
38+
`
39+
);
40+
}
41+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<script>
2+
let value = $state(false);
3+
const fn = () => {
4+
if ($effect.active()) {
5+
$effect(() => {
6+
value = true;
7+
});
8+
}
9+
return value;
10+
};
11+
12+
let outer = $state(false);
13+
let inner = $state(false);
14+
let v = $derived(inner ? fn() : false);
15+
</script>
16+
17+
<button onclick={() => outer = !outer}>
18+
toggle outer
19+
</button>
20+
21+
<button onclick={() => inner = !inner}>
22+
toggle inner
23+
</button>
24+
25+
<button onclick={() => outer = inner = value = false}>
26+
reset
27+
</button>
28+
29+
{#if outer && v}
30+
<p>v is true</p>
31+
{/if}

0 commit comments

Comments
 (0)