From 64fa513e8bf2ef9b7adb3d177e5083ebb80be5bb Mon Sep 17 00:00:00 2001
From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com>
Date: Wed, 16 Jul 2025 17:50:01 -0700
Subject: [PATCH 01/10] init
---
.../phases/2-analyze/visitors/CallExpression.js | 1 +
.../3-transform/client/visitors/CallExpression.js | 3 +++
.../3-transform/server/visitors/CallExpression.js | 2 +-
packages/svelte/src/internal/client/index.js | 1 +
.../src/internal/client/reactivity/effects.js | 14 ++++++++++++++
packages/svelte/src/utils.js | 1 +
6 files changed, 21 insertions(+), 1 deletion(-)
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..25cff4bfe093 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.active':
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..746c051e3f30 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.active':
+ return b.call('$.effect_active');
+
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..64d4f573c586 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.active') {
return b.false;
}
diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js
index cddb432a982b..a186cdd86bbf 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_active,
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..ccc2d9c9ec44 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -165,6 +165,20 @@ export function effect_tracking() {
return active_reaction !== null && !untracking;
}
+/**
+ * Internal representation of `$effect.active()`
+ * @returns {boolean}
+ */
+export function effect_active() {
+ if (active_reaction === null && active_effect === null) {
+ return false;
+ }
+ if (is_destroying_effect) {
+ return false;
+ }
+ return true;
+}
+
/**
* @param {() => void} fn
*/
diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js
index cd79cfc27467..f49382921a5b 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.active',
'$effect.pre',
'$effect.tracking',
'$effect.root',
From 45d3eeb4e6c905f44f1328200f7dc8e0c0e874b4 Mon Sep 17 00:00:00 2001
From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com>
Date: Fri, 18 Jul 2025 03:00:43 -0700
Subject: [PATCH 02/10] add types, DRY out some logic
---
packages/svelte/src/ambient.d.ts | 28 ++++++++++++
.../svelte/src/internal/client/constants.js | 5 +++
.../src/internal/client/reactivity/effects.js | 43 +++++++++++++------
packages/svelte/types/index.d.ts | 28 ++++++++++++
4 files changed, 91 insertions(+), 13 deletions(-)
diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts
index 7c3b941ed1fb..aaaddbe544ff 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.active` 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.active
+ */
+ export function active(): 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/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js
index 50a7a21ae80f..de0ca92f1289 100644
--- a/packages/svelte/src/internal/client/constants.js
+++ b/packages/svelte/src/internal/client/constants.js
@@ -41,3 +41,8 @@ export const ELEMENT_NODE = 1;
export const TEXT_NODE = 3;
export const COMMENT_NODE = 8;
export const DOCUMENT_FRAGMENT_NODE = 11;
+
+export const VALID_EFFECT_PARENT = 0;
+export const EFFECT_ORPHAN = 1;
+export const UNOWNED_DERIVED_PARENT = 2;
+export const EFFECT_TEARDOWN = 3;
\ No newline at end of file
diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index ccc2d9c9ec44..7921b1e5e91f 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -33,7 +33,11 @@ import {
EFFECT_PRESERVED,
STALE_REACTION,
USER_EFFECT,
- ASYNC
+ ASYNC,
+ EFFECT_ORPHAN,
+ EFFECT_TEARDOWN,
+ UNOWNED_DERIVED_PARENT,
+ VALID_EFFECT_PARENT
} from '#client/constants';
import * as e from '../errors.js';
import { DEV } from 'esm-env';
@@ -44,19 +48,38 @@ import { Batch, schedule_effect } from './batch.js';
import { flatten } from './async.js';
/**
- * @param {'$effect' | '$effect.pre' | '$inspect'} rune
+ * @returns {number}
*/
-export function validate_effect(rune) {
+function active_root_effect() {
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 = active_root_effect();
+ switch(valid_effect_parent) {
+ case VALID_EFFECT_PARENT:
+ return;
+ case EFFECT_ORPHAN:
+ e.effect_orphan(rune);
+ case UNOWNED_DERIVED_PARENT:
+ e.effect_in_unowned_derived();
+ case EFFECT_TEARDOWN:
+ e.effect_in_teardown(rune);
}
}
@@ -170,13 +193,7 @@ export function effect_tracking() {
* @returns {boolean}
*/
export function effect_active() {
- if (active_reaction === null && active_effect === null) {
- return false;
- }
- if (is_destroying_effect) {
- return false;
- }
- return true;
+ return active_root_effect() === VALID_EFFECT_PARENT;
}
/**
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index a8b769d6d4c3..abe0213ed354 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.active` 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.active
+ */
+ export function active(): 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.
From 9ae4a65c4ac1d8a0ba8069ac3a1da3889dca61a9 Mon Sep 17 00:00:00 2001
From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com>
Date: Fri, 18 Jul 2025 18:26:56 -0700
Subject: [PATCH 03/10] add docs
---
documentation/docs/02-runes/04-$effect.md | 24 +++++++++++++++++++
.../src/internal/client/reactivity/effects.js | 2 ++
2 files changed, 26 insertions(+)
diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md
index 5820e178a078..4a27a7277851 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.active`
+
+The `$effect.active` 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/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index 7921b1e5e91f..75f8aa22f3d3 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -48,6 +48,8 @@ import { Batch, schedule_effect } from './batch.js';
import { flatten } from './async.js';
/**
+ * 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}
*/
function active_root_effect() {
From c61f2a9bae8176b9e9aa74e4206c17720a82d8c9 Mon Sep 17 00:00:00 2001
From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com>
Date: Sat, 19 Jul 2025 10:27:25 -0700
Subject: [PATCH 04/10] changeset
---
.changeset/two-dancers-speak.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/two-dancers-speak.md
diff --git a/.changeset/two-dancers-speak.md b/.changeset/two-dancers-speak.md
new file mode 100644
index 000000000000..7044eaa329d3
--- /dev/null
+++ b/.changeset/two-dancers-speak.md
@@ -0,0 +1,5 @@
+---
+'svelte': minor
+---
+
+feat: add `$effect.active` rune
From 9c1de633c0812f47eca2e8c9c5c1366eb9f2212b Mon Sep 17 00:00:00 2001
From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com>
Date: Sat, 19 Jul 2025 10:41:47 -0700
Subject: [PATCH 05/10] add test
---
packages/svelte/tests/signals/test.ts | 38 +++++++++++++++++++++++++++
1 file changed, 38 insertions(+)
diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts
index 937324727b16..261198fc1c87 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_active,
effect_root,
render_effect,
user_effect,
@@ -1390,4 +1391,41 @@ describe('signals', () => {
destroy();
};
});
+
+ test('$effect.active()', () => {
+ const log: Array = [];
+
+ return () => {
+ log.push('effect orphan', effect_active());
+ const destroy = effect_root(() => {
+ log.push('effect root', effect_active());
+ effect(() => {
+ log.push('effect', effect_active());
+ });
+ $.get(
+ derived(() => {
+ log.push('derived', effect_active());
+ return 1;
+ })
+ );
+ return () => {
+ log.push('effect teardown', effect_active());
+ };
+ });
+ flushSync();
+ destroy();
+ assert.deepEqual(log, [
+ 'effect orphan',
+ false,
+ 'effect root',
+ true,
+ 'derived',
+ true,
+ 'effect',
+ true,
+ 'effect teardown',
+ false
+ ]);
+ };
+ });
});
From c82fb3a0c83b7aed53183c61b26ab7a699089dfa Mon Sep 17 00:00:00 2001
From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com>
Date: Sun, 20 Jul 2025 13:57:48 -0700
Subject: [PATCH 06/10] remove obsolete test
---
.../compiler-errors/samples/effect-active-rune/_config.js | 8 --------
.../samples/effect-active-rune/main.svelte.js | 1 -
2 files changed, 9 deletions(-)
delete mode 100644 packages/svelte/tests/compiler-errors/samples/effect-active-rune/_config.js
delete mode 100644 packages/svelte/tests/compiler-errors/samples/effect-active-rune/main.svelte.js
diff --git a/packages/svelte/tests/compiler-errors/samples/effect-active-rune/_config.js b/packages/svelte/tests/compiler-errors/samples/effect-active-rune/_config.js
deleted file mode 100644
index e12e4046f36d..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/effect-active-rune/_config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { test } from '../../test';
-
-export default test({
- error: {
- code: 'rune_renamed',
- message: '`$effect.active` is now `$effect.tracking`'
- }
-});
diff --git a/packages/svelte/tests/compiler-errors/samples/effect-active-rune/main.svelte.js b/packages/svelte/tests/compiler-errors/samples/effect-active-rune/main.svelte.js
deleted file mode 100644
index c33c104aac31..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/effect-active-rune/main.svelte.js
+++ /dev/null
@@ -1 +0,0 @@
-$effect.active();
From 7b33b2ab54ce4e1710e89e1120a100e00ceaefc4 Mon Sep 17 00:00:00 2001
From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com>
Date: Sun, 20 Jul 2025 14:04:46 -0700
Subject: [PATCH 07/10] fix lint
---
.../svelte/src/internal/client/reactivity/effects.js | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index 75f8aa22f3d3..5d0b24e33d4e 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -48,8 +48,8 @@ import { Batch, schedule_effect } from './batch.js';
import { flatten } from './async.js';
/**
- * If an effect can be created in the current context, `VALID_EFFECT_PARENT` is returned.
- * If not, a value indicating why is returned.
+ * 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}
*/
function active_root_effect() {
@@ -73,15 +73,18 @@ function active_root_effect() {
*/
export function validate_effect(rune) {
const valid_effect_parent = active_root_effect();
- switch(valid_effect_parent) {
+ 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;
}
}
From ab91795ac8b2bfefac3e72e2f24b6a0e40d16f91 Mon Sep 17 00:00:00 2001
From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com>
Date: Sun, 20 Jul 2025 14:13:23 -0700
Subject: [PATCH 08/10] lint
---
packages/svelte/src/ambient.d.ts | 6 +++---
packages/svelte/src/internal/client/constants.js | 2 +-
packages/svelte/types/index.d.ts | 6 +++---
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts
index 5bf69a157fe2..cc395995cc3c 100644
--- a/packages/svelte/src/ambient.d.ts
+++ b/packages/svelte/src/ambient.d.ts
@@ -240,12 +240,12 @@ declare namespace $effect {
/**
* The `$effect.active` 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
*