Skip to content

feat: add $effect.allowed rune #16458

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.allowed` 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.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
<script>
console.log('in component setup', $effect.allowed()); // true

function onclick() {
console.log('after component setup', $effect.allowed()); // false
}
function ondblclick() {
$effect.root(() => {
console.log('in root effect', $effect.allowed()); // true
return () => {
console.log('in effect teardown', $effect.allowed()); // 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.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
* <script>
* console.log('in component setup', $effect.allowed()); // true
*
* function onclick() {
* console.log('after component setup', $effect.allowed()); // false
* }
* function ondblclick() {
* $effect.root(() => {
* console.log('in root effect', $effect.allowed()); // true
* return () => {
* console.log('in effect teardown', $effect.allowed()); // false
* }
* })();
* }
* </script>
* <button {onclick}>Click me!</button>
* <button {ondblclick}>Click me twice!</button>
* ```
*
* 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.
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.allowed':
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.allowed':
return b.call('$.effect_allowed');

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.allowed') {
return b.false;
}

Expand Down
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_allowed,
effect_tracking,
effect_root,
legacy_pre_effect,
Expand Down
47 changes: 42 additions & 5 deletions packages/svelte/src/internal/client/reactivity/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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
*/
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.allowed',
'$effect.pre',
'$effect.tracking',
'$effect.root',
Expand Down
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_allowed,
effect_root,
render_effect,
user_effect,
Expand Down Expand Up @@ -1390,4 +1391,41 @@ describe('signals', () => {
destroy();
};
});

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

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
]);
};
});
});
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.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
* <script>
* console.log('in component setup', $effect.allowed()); // true
*
* function onclick() {
* console.log('after component setup', $effect.allowed()); // false
* }
* function ondblclick() {
* $effect.root(() => {
* console.log('in root effect', $effect.allowed()); // true
* return () => {
* console.log('in effect teardown', $effect.allowed()); // false
* }
* })();
* }
* </script>
* <button {onclick}>Click me!</button>
* <button {ondblclick}>Click me twice!</button>
* ```
*
* 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.
Expand Down
Loading