pull/16458/merge
ComputerGuy 4 days ago committed by GitHub
commit 1dcfc0836b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: add `$effect.allowed` rune

@ -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...

@ -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.

@ -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);

@ -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');

@ -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;
}

@ -112,6 +112,7 @@ export {
} from './reactivity/deriveds.js';
export {
aborted,
effect_allowed,
effect_tracking,
effect_root,
legacy_pre_effect,

@ -44,20 +44,49 @@ import { Batch, schedule_effect } from './batch.js';
import { flatten } from './async.js';
import { without_reactive_context } from '../dom/elements/bindings/shared.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;
}
}
@ -181,6 +210,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
*/

@ -442,6 +442,7 @@ const RUNES = /** @type {const} */ ([
'$props.id',
'$bindable',
'$effect',
'$effect.allowed',
'$effect.pre',
'$effect.tracking',
'$effect.root',

@ -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,
@ -1391,7 +1392,44 @@ describe('signals', () => {
};
});
test('$effect.root inside deriveds stay alive independently', () => {
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
]);
};
});
test('$effect.root inside deriveds stay alive independently', () => {
const log: any[] = [];
const c = state(0);
const cleanup: any[] = [];

@ -3321,6 +3321,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.

Loading…
Cancel
Save