Skip to content
5 changes: 5 additions & 0 deletions .changeset/two-dancers-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: add `$effect.active` rune
24 changes: 24 additions & 0 deletions documentation/docs/02-runes/04-$effect.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<script>
console.log('in component setup', $effect.active()); // true

function onclick() {
console.log('after component setup', $effect.active()); // false
}
function ondblclick() {
$effect.root(() => {
console.log('in root effect', $effect.active()); // true
return () => {
console.log('in effect teardown', $effect.active()); // false
}
})();
}
</script>
<button {onclick}>Click me!</button>
<button {ondblclick}>Click me twice!</button>
```

## 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...
Expand Down
28 changes: 28 additions & 0 deletions packages/svelte/src/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <script>
* console.log('in component setup', $effect.active()); // true
*
* function onclick() {
* console.log('after component setup', $effect.active()); // false
* }
* function ondblclick() {
* $effect.root(() => {
* console.log('in root effect', $effect.active()); // true
* return () => {
* console.log('in effect teardown', $effect.active()); // false
* }
* })();
* }
* </script>
* <button {onclick}>Click me!</button>
* <button {ondblclick}>Click me twice!</button>
* ```
*
* 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
5 changes: 5 additions & 0 deletions packages/svelte/src/internal/client/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
1 change: 1 addition & 0 deletions packages/svelte/src/internal/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export {
} from './reactivity/deriveds.js';
export {
aborted,
effect_active,
effect_tracking,
effect_root,
legacy_pre_effect,
Expand Down
48 changes: 42 additions & 6 deletions packages/svelte/src/internal/client/reactivity/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -44,19 +48,43 @@ import { Batch, schedule_effect } from './batch.js';
import { flatten } from './async.js';

/**
* @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 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);
break;
case UNOWNED_DERIVED_PARENT:
e.effect_in_unowned_derived();
break;
case EFFECT_TEARDOWN:
e.effect_in_teardown(rune);
break;
}
}

Expand Down Expand Up @@ -165,6 +193,14 @@ export function effect_tracking() {
return active_reaction !== null && !untracking;
}

/**
* Internal representation of `$effect.active()`
* @returns {boolean}
*/
export function effect_active() {
return active_root_effect() === VALID_EFFECT_PARENT;
}

/**
* @param {() => void} fn
*/
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ const RUNES = /** @type {const} */ ([
'$props.id',
'$bindable',
'$effect',
'$effect.active',
'$effect.pre',
'$effect.tracking',
'$effect.root',
Expand Down

This file was deleted.

This file was deleted.

38 changes: 38 additions & 0 deletions packages/svelte/tests/signals/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1390,4 +1391,41 @@ describe('signals', () => {
destroy();
};
});

test('$effect.active()', () => {
const log: Array<string | boolean> = [];

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
]);
};
});
});
28 changes: 28 additions & 0 deletions packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <script>
* console.log('in component setup', $effect.active()); // true
*
* function onclick() {
* console.log('after component setup', $effect.active()); // false
* }
* function ondblclick() {
* $effect.root(() => {
* console.log('in root effect', $effect.active()); // true
* return () => {
* console.log('in effect teardown', $effect.active()); // false
* }
* })();
* }
* </script>
* <button {onclick}>Click me!</button>
* <button {ondblclick}>Click me twice!</button>
* ```
*
* 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.
Expand Down
Loading