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 <dg@domgan.com>
pull/11518/head
Rich Harris 8 months ago committed by GitHub
parent 30caaef2e5
commit 8742823e39
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make `$effect.active()` true when updating deriveds

@ -24,6 +24,10 @@
> `%rune%` cannot be used inside an effect cleanup function > `%rune%` cannot be used inside an effect cleanup function
## effect_in_unowned_derived
> Effect cannot be created inside a `$derived` value that was not itself created inside an effect
## effect_orphan ## effect_orphan
> `%rune%` can only be used inside an effect (e.g. during component initialisation) > `%rune%` can only be used inside an effect (e.g. during component initialisation)

@ -20,7 +20,7 @@ export let inspect_captured_signals = [];
*/ */
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
export function inspect(get_value, inspector = console.log) { export function inspect(get_value, inspector = console.log) {
validate_effect(current_effect, '$inspect'); validate_effect('$inspect');
let initial = true; let initial = true;

@ -111,6 +111,22 @@ export function effect_in_teardown(rune) {
} }
} }
/**
* Effect cannot be created inside a `$derived` value that was not itself created inside an effect
* @returns {never}
*/
export function effect_in_unowned_derived() {
if (DEV) {
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"}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("effect_in_unowned_derived");
}
}
/** /**
* `%rune%` can only be used inside an effect (e.g. during component initialisation) * `%rune%` can only be used inside an effect (e.g. during component initialisation)
* @param {string} rune * @param {string} rune

@ -121,6 +121,7 @@ export function update_derived(derived, force_schedule) {
*/ */
export function destroy_derived(signal) { export function destroy_derived(signal) {
destroy_derived_children(signal); destroy_derived_children(signal);
destroy_effect_children(signal);
remove_reactions(signal, 0); remove_reactions(signal, 0);
set_signal_status(signal, DESTROYED); set_signal_status(signal, DESTROYED);

@ -26,7 +26,9 @@ import {
EFFECT_RAN, EFFECT_RAN,
BLOCK_EFFECT, BLOCK_EFFECT,
ROOT_EFFECT, ROOT_EFFECT,
EFFECT_TRANSPARENT EFFECT_TRANSPARENT,
DERIVED,
UNOWNED
} from '../constants.js'; } from '../constants.js';
import { set } from './sources.js'; import { set } from './sources.js';
import { remove } from '../dom/reconciler.js'; import { remove } from '../dom/reconciler.js';
@ -34,12 +36,10 @@ import * as e from '../errors.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
/** /**
* @param {import('#client').Effect | null} effect
* @param {'$effect' | '$effect.pre' | '$inspect'} rune * @param {'$effect' | '$effect.pre' | '$inspect'} rune
* @returns {asserts effect}
*/ */
export function validate_effect(effect, rune) { export function validate_effect(rune) {
if (effect === null) { if (current_effect === null && current_reaction === null) {
e.effect_orphan(rune); e.effect_orphan(rune);
} }
@ -93,6 +93,18 @@ function create_effect(type, fn, sync) {
} }
if (current_reaction !== null && !is_root) { if (current_reaction !== null && !is_root) {
var flags = current_reaction.f;
if ((flags & DERIVED) !== 0) {
if ((flags & UNOWNED) !== 0) {
e.effect_in_unowned_derived();
}
// If we are inside a derived, then we also need to attach the
// effect to the parent effect too.
if (current_effect !== null) {
push_effect(effect, current_effect);
}
}
push_effect(effect, current_reaction); push_effect(effect, current_reaction);
} }
@ -118,7 +130,15 @@ function create_effect(type, fn, sync) {
* @returns {boolean} * @returns {boolean}
*/ */
export function effect_active() { export function effect_active() {
return current_effect ? (current_effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 : false; if (current_reaction && (current_reaction.f & DERIVED) !== 0) {
return (current_reaction.f & UNOWNED) === 0;
}
if (current_effect) {
return (current_effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0;
}
return false;
} }
/** /**
@ -126,12 +146,13 @@ export function effect_active() {
* @param {() => void | (() => void)} fn * @param {() => void | (() => void)} fn
*/ */
export function user_effect(fn) { export function user_effect(fn) {
validate_effect(current_effect, '$effect'); validate_effect('$effect');
// Non-nested `$effect(...)` in a component should be deferred // Non-nested `$effect(...)` in a component should be deferred
// until the component is mounted // until the component is mounted
const defer = const defer =
current_effect.f & RENDER_EFFECT && current_effect !== null &&
(current_effect.f & RENDER_EFFECT) !== 0 &&
// TODO do we actually need this? removing them changes nothing // TODO do we actually need this? removing them changes nothing
current_component_context !== null && current_component_context !== null &&
!current_component_context.m; !current_component_context.m;
@ -150,7 +171,7 @@ export function user_effect(fn) {
* @returns {import('#client').Effect} * @returns {import('#client').Effect}
*/ */
export function user_pre_effect(fn) { export function user_pre_effect(fn) {
validate_effect(current_effect, '$effect.pre'); validate_effect('$effect.pre');
return render_effect(fn); return render_effect(fn);
} }

@ -0,0 +1,41 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `
<button>toggle outer</button>
<button>toggle inner</button>
<button>reset</button>
`,
test({ assert, target }) {
const [outer, inner, reset] = target.querySelectorAll('button');
flushSync(() => outer?.click());
flushSync(() => inner?.click());
assert.htmlEqual(
target.innerHTML,
`
<button>toggle outer</button>
<button>toggle inner</button>
<button>reset</button>
<p>v is true</p>
`
);
flushSync(() => reset?.click());
flushSync(() => inner?.click());
flushSync(() => outer?.click());
assert.htmlEqual(
target.innerHTML,
`
<button>toggle outer</button>
<button>toggle inner</button>
<button>reset</button>
<p>v is true</p>
`
);
}
});

@ -0,0 +1,31 @@
<script>
let value = $state(false);
const fn = () => {
if ($effect.active()) {
$effect(() => {
value = true;
});
}
return value;
};
let outer = $state(false);
let inner = $state(false);
let v = $derived(inner ? fn() : false);
</script>
<button onclick={() => outer = !outer}>
toggle outer
</button>
<button onclick={() => inner = !inner}>
toggle inner
</button>
<button onclick={() => outer = inner = value = false}>
reset
</button>
{#if outer && v}
<p>v is true</p>
{/if}
Loading…
Cancel
Save