ComputerGuy 2 days ago committed by GitHub
commit ff2028a549
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

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

@ -204,24 +204,6 @@ In rare cases, you may need to run code _before_ the DOM updates. For this we ca
Apart from the timing, `$effect.pre` works exactly like `$effect`.
## `$effect.tracking`
The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template ([demo](/playground/untitled#H4sIAAAAAAAACn3PwYrCMBDG8VeZDYIt2PYeY8Dn2HrIhqkU08nQjItS-u6buAt7UDzmz8ePyaKGMWBS-nNRcmdU-hHUTpGbyuvI3KZvDFLal0v4qvtIgiSZUSb5eWSxPfWSc4oB2xDP1XYk8HHiSHkICeXKeruDDQ4Demlldv4y0rmq6z10HQwuJMxGVv4mVVXDwcJS0jP9u3knynwtoKz1vifT_Z9Jhm0WBCcOTlDD8kyspmML5qNpHg40jc3fFryJ0iWsp_UHgz3180oBAAA=)):
```svelte
<script>
console.log('in component setup:', $effect.tracking()); // false
$effect(() => {
console.log('in effect:', $effect.tracking()); // true
});
</script>
<p>in template: {$effect.tracking()}</p> <!-- true -->
```
It is used to implement abstractions like [`createSubscriber`](/docs/svelte/svelte-reactivity#createSubscriber), which will create listeners to update reactive values but _only_ if those values are being tracked (rather than, for example, read inside an event handler).
## `$effect.pending`
When using [`await`](await-expressions) in components, the `$effect.pending()` rune tells you how many promises are pending in the current [boundary](svelte-boundary), not including child boundaries ([demo](/playground/untitled#H4sIAAAAAAAAE3WRMU_DMBCF_8rJdHDUqilILGkaiY2RgY0yOPYZWbiOFV8IleX_jpMUEAIWS_7u-d27c2ROnJBV7B6t7WDsequAozKEqmAbpo3FwKqnyOjsJ90EMr-8uvN-G97Q0sRaEfAvLjtH6CjbsDrI3nhqju5IFgkEHGAVSBDy62L_SdtvejPTzEU4Owl6cJJM50AoxcUG2gLiVM31URgChyM89N3JBORcF3BoICA9mhN2A3G9gdvdrij2UJYgejLaSCMsKLTivNj0SEOf7WEN7ZwnHV1dfqd2dTsQ5QCdk9bI10PkcxexXqcmH3W51Jt_le2kbH8os9Y3UaTcNLYpDx-Xab6GTHXpZ128MhpWqDVK2np0yrgXXqQpaLa4APDLBkIF8bd2sYql0Sn_DeE7sYr6AdNzvgljR-MUq7SwAdMHeUtgHR4CAAA=)):
@ -256,6 +238,54 @@ const destroy = $effect.root(() => {
destroy();
```
Effect roots are also created when you [`mount`](imperative-component-api#mount) or [`hydrate`](imperative-component-api#hydrate) a component.
## `$effect.tracking`
The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template ([demo](/playground/untitled#H4sIAAAAAAAACn3PwYrCMBDG8VeZDYIt2PYeY8Dn2HrIhqkU08nQjItS-u6buAt7UDzmz8ePyaKGMWBS-nNRcmdU-hHUTpGbyuvI3KZvDFLal0v4qvtIgiSZUSb5eWSxPfWSc4oB2xDP1XYk8HHiSHkICeXKeruDDQ4Demlldv4y0rmq6z10HQwuJMxGVv4mVVXDwcJS0jP9u3knynwtoKz1vifT_Z9Jhm0WBCcOTlDD8kyspmML5qNpHg40jc3fFryJ0iWsp_UHgz3180oBAAA=)):
```svelte
<script>
console.log('in component setup:', $effect.tracking()); // false
$effect(() => {
console.log('in effect:', $effect.tracking()); // true
});
</script>
<p>in template: {$effect.tracking()}</p> <!-- true -->
```
It is used to implement abstractions like [`createSubscriber`](/docs/svelte/svelte-reactivity#createSubscriber), which will create listeners to update reactive values but _only_ if those values are being tracked (rather than, for example, read inside an event handler).
## `$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 other words, that we are inside the context of an effect root.
```svelte
<script>
console.log('in component setup', $effect.allowed()); // true
function onclick() {
console.log('after component setup', $effect.allowed()); // false
const destroy = $effect.root(() => {
console.log('in root effect', $effect.allowed()); // true
return () => {
console.log('in effect teardown', $effect.allowed()); // false
};
});
destroy();
}
</script>
<button {onclick}>Click me!</button>
```
The difference between `$effect.allowed()` and `$effect.tracking()` is that `$effect.tracking()` will return `false` at the top level of a component (or directly inside an effect root), since reading state at that moment will _not_ cause anything to re-run when the state changes.
## 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...

@ -257,6 +257,36 @@ 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
*
* const destroy = $effect.root(() => {
* console.log('in root effect', $effect.allowed()); // true
*
* return () => {
* console.log('in effect teardown', $effect.allowed()); // false
* };
* });
*
* destroy();
* }
* </script>
*
* <button {onclick}>Click me!</button>
* ```
*
* @see {@link https://svelte.dev/docs/svelte/$effect#$effect.allowed Documentation}
*/
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');

@ -23,7 +23,7 @@ export function CallExpression(node, context) {
return b.void0;
}
if (rune === '$effect.tracking') {
if (rune === '$effect.tracking' || rune === '$effect.allowed') {
return b.false;
}

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

@ -179,6 +179,14 @@ export function effect_tracking() {
return active_reaction !== null && !untracking;
}
/**
* Internal representation of `$effect.allowed()`
* @returns {boolean}
*/
export function effect_allowed() {
return active_effect !== null && !is_destroying_effect;
}
/**
* @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,6 +1392,43 @@ describe('signals', () => {
};
});
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('derived whose original parent effect has been destroyed keeps updating', () => {
return () => {
let count: Source<number>;

@ -3446,6 +3446,36 @@ 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
*
* const destroy = $effect.root(() => {
* console.log('in root effect', $effect.allowed()); // true
*
* return () => {
* console.log('in effect teardown', $effect.allowed()); // false
* };
* });
*
* destroy();
* }
* </script>
*
* <button {onclick}>Click me!</button>
* ```
*
* @see {@link https://svelte.dev/docs/svelte/$effect#$effect.allowed Documentation}
*/
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