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.