diff --git a/.changeset/two-dancers-speak.md b/.changeset/two-dancers-speak.md new file mode 100644 index 000000000000..b0d8d394d0dd --- /dev/null +++ b/.changeset/two-dancers-speak.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add `$effect.allowed` rune diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index 5820e178a078..59010c981fca 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -255,6 +255,30 @@ const destroy = $effect.root(() => { destroy(); ``` +## `$effect.allowed` + +The `$effect.allowed` rune is an advanced feature that indicates whether or not an effect or [async `$derived`](await-expressions) can be created in the current context. To improve performance and memory efficiency, effects and async deriveds can only be created when a root effect is active. Root effects are created during component setup, but they can also be programmatically created via `$effect.root`. + +```svelte + + + +``` + ## When not to use `$effect` In general, `$effect` is best considered something of an escape hatch — useful for things like analytics and direct DOM manipulation — rather than a tool you should use frequently. In particular, avoid using it to synchronise state. Instead of this... diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index ad32eaa56f5e..5245626d53b4 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -237,6 +237,34 @@ declare namespace $derived { declare function $effect(fn: () => void | (() => void)): void; declare namespace $effect { + /** + * The `$effect.allowed` rune is an advanced feature that indicates whether an effect or async `$derived` can be created in the current context. + * Effects and async deriveds can only be created in root effects, which are created during component setup, or can be programmatically created via `$effect.root`. + * + * Example: + * ```svelte + * + * + * + * ``` + * + * https://svelte.dev/docs/svelte/$effect#$effect.allowed + */ + export function allowed(): boolean; /** * Runs code right before a component is mounted to the DOM, and then whenever its dependencies change, i.e. `$state` or `$derived` values. * The timing of the execution is right before the DOM is updated. diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 9b6337b9ed9a..b4c5d4fdc400 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -150,6 +150,7 @@ export function CallExpression(node, context) { break; + case '$effect.allowed': case '$effect.tracking': if (node.arguments.length !== 0) { e.rune_invalid_arguments(node, rune); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index 3e2f1414e63b..1b5c2fa2dae5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -17,6 +17,9 @@ export function CallExpression(node, context) { case '$host': return b.id('$$props.$$host'); + case '$effect.allowed': + return b.call('$.effect_allowed'); + case '$effect.tracking': return b.call('$.effect_tracking'); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index 41d3202ce9ea..e4c6fc66ab54 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -16,7 +16,7 @@ export function CallExpression(node, context) { return b.void0; } - if (rune === '$effect.tracking') { + if (rune === '$effect.tracking' || rune === '$effect.allowed') { return b.false; } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index cddb432a982b..a38f2f4173f8 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -107,6 +107,7 @@ export { } from './reactivity/deriveds.js'; export { aborted, + effect_allowed, effect_tracking, effect_root, legacy_pre_effect, diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index c4edd2bf8d95..18167d6bf5e8 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -43,20 +43,49 @@ import { component_context, dev_current_component_function, dev_stack } from '.. import { Batch, schedule_effect } from './batch.js'; import { flatten } from './async.js'; +const VALID_EFFECT_PARENT = 0; +const EFFECT_ORPHAN = 1; +const UNOWNED_DERIVED_PARENT = 2; +const EFFECT_TEARDOWN = 3; + /** - * @param {'$effect' | '$effect.pre' | '$inspect'} rune + * If an effect can be created in the current context, `VALID_EFFECT_PARENT` is returned. + * If not, a value indicating why is returned. + * @returns {number} */ -export function validate_effect(rune) { +function valid_effect_creation_context() { if (active_effect === null && active_reaction === null) { - e.effect_orphan(rune); + return EFFECT_ORPHAN; } if (active_reaction !== null && (active_reaction.f & UNOWNED) !== 0 && active_effect === null) { - e.effect_in_unowned_derived(); + return UNOWNED_DERIVED_PARENT; } if (is_destroying_effect) { - e.effect_in_teardown(rune); + return EFFECT_TEARDOWN; + } + + return VALID_EFFECT_PARENT; +} + +/** + * @param {'$effect' | '$effect.pre' | '$inspect'} rune + */ +export function validate_effect(rune) { + const valid_effect_parent = valid_effect_creation_context(); + switch (valid_effect_parent) { + case VALID_EFFECT_PARENT: + return; + case EFFECT_ORPHAN: + e.effect_orphan(rune); + break; + case UNOWNED_DERIVED_PARENT: + e.effect_in_unowned_derived(); + break; + case EFFECT_TEARDOWN: + e.effect_in_teardown(rune); + break; } } @@ -165,6 +194,14 @@ export function effect_tracking() { return active_reaction !== null && !untracking; } +/** + * Internal representation of `$effect.allowed()` + * @returns {boolean} + */ +export function effect_allowed() { + return valid_effect_creation_context() === VALID_EFFECT_PARENT; +} + /** * @param {() => void} fn */ diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index cd79cfc27467..231c10c7cf5b 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -442,6 +442,7 @@ const RUNES = /** @type {const} */ ([ '$props.id', '$bindable', '$effect', + '$effect.allowed', '$effect.pre', '$effect.tracking', '$effect.root', diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 937324727b16..32aed75b658f 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -4,6 +4,7 @@ import * as $ from '../../src/internal/client/runtime'; import { push, pop } from '../../src/internal/client/context'; import { effect, + effect_allowed, effect_root, render_effect, user_effect, @@ -1390,4 +1391,41 @@ describe('signals', () => { destroy(); }; }); + + test('$effect.allowed()', () => { + const log: Array = []; + + return () => { + log.push('effect orphan', effect_allowed()); + const destroy = effect_root(() => { + log.push('effect root', effect_allowed()); + effect(() => { + log.push('effect', effect_allowed()); + }); + $.get( + derived(() => { + log.push('derived', effect_allowed()); + return 1; + }) + ); + return () => { + log.push('effect teardown', effect_allowed()); + }; + }); + flushSync(); + destroy(); + assert.deepEqual(log, [ + 'effect orphan', + false, + 'effect root', + true, + 'derived', + true, + 'effect', + true, + 'effect teardown', + false + ]); + }; + }); }); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 9ea45af7e6b7..e1ac8031985a 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -3311,6 +3311,34 @@ declare namespace $derived { declare function $effect(fn: () => void | (() => void)): void; declare namespace $effect { + /** + * The `$effect.allowed` rune is an advanced feature that indicates whether an effect or async `$derived` can be created in the current context. + * Effects and async deriveds can only be created in root effects, which are created during component setup, or can be programmatically created via `$effect.root`. + * + * Example: + * ```svelte + * + * + * + * ``` + * + * https://svelte.dev/docs/svelte/$effect#$effect.allowed + */ + export function allowed(): boolean; /** * Runs code right before a component is mounted to the DOM, and then whenever its dependencies change, i.e. `$state` or `$derived` values. * The timing of the execution is right before the DOM is updated.